From 68f856fb9d5860551cdb313fefd0da3cb111e7b7 Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Mon, 28 Sep 2026 11:47:35 +0000 Subject: [PATCH] feat(core): Add production n8n Chat channel infrastructure (no-changelog) (#39476) Co-authored-by: Benjamin Schroth --- biome.jsonc | 2 + .../agent-integration.schema.test.ts | 21 +- .../src/agents/agent-config-lifecycle.ts | 6 +- .../src/agents/agent-integration.schema.ts | 26 +- .../src/agents/agent-json-config.schema.ts | 10 +- packages/@n8n/api-types/src/agents/dto.ts | 1 + packages/@n8n/api-types/src/agents/types.ts | 5 +- .../agent-chat-execution.service.test.ts | 48 ++ .../__tests__/agent-chat.controller.test.ts | 132 +++++- .../__tests__/agent-config.service.test.ts | 23 + ...ent-execution-orchestrator.service.test.ts | 162 +++++++ .../__tests__/agent-execution.service.test.ts | 47 ++ .../agent-integrations.controller.test.ts | 33 ++ .../__tests__/agent-publish.service.test.ts | 21 +- .../agent-runtime-cache.service.test.ts | 45 ++ ...time-reconstruction-tool-filtering.test.ts | 76 ++- .../agent-validation.service.test.ts | 35 ++ ...agent-workflow-tool-resume.service.test.ts | 39 ++ .../agents/agent-chat-execution.service.ts | 43 +- .../modules/agents/agent-chat.controller.ts | 314 ++++++++++++- .../modules/agents/agent-config.service.ts | 8 +- .../agent-execution-orchestrator.service.ts | 213 +++++++-- .../modules/agents/agent-execution.service.ts | 21 + .../agents/agent-integrations.controller.ts | 22 +- .../modules/agents/agent-publish.service.ts | 34 +- .../agents/agent-runtime-cache.service.ts | 5 +- .../agent-runtime-reconstruction.service.ts | 19 +- .../agents/agent-turn-execution.service.ts | 12 +- .../agents/agent-validation.service.ts | 6 +- .../agent-workflow-tool-resume.service.ts | 53 +++ .../cli/src/modules/agents/agents.service.ts | 16 +- .../builder/agents-builder-tools.service.ts | 11 +- .../__tests__/channel-status-report.test.ts | 15 + .../chat-integration.service.test.ts | 12 + .../agent-channel-reconciler.service.ts | 11 +- .../agents/integrations/agent-chat-bridge.ts | 5 +- .../integrations/channel-status-report.ts | 12 +- .../integrations/chat-integration.service.ts | 22 +- .../integrations/integration-tool-factory.ts | 4 +- .../integrations/integration-tool-types.ts | 5 +- .../platforms/n8n-chat-integration.ts | 8 +- .../json-config/agent-config-composition.ts | 5 +- .../agents/repositories/agent.repository.ts | 10 + .../__tests__/workflow-tool-factory.test.ts | 16 + .../agents/tools/workflow-tool-factory.ts | 2 + .../agents/utils/agent-memory-scope.ts | 11 + .../agents/utils/agent-thread-access.ts | 1 + .../src/scaling/pubsub/pubsub.event-map.ts | 1 + ...ssion-owner-to-cancel-a-suspended-run.json | 12 + ...del-and-reloads-the-stored-attachment.json | 22 + ...-records-a-private-production-session.json | 42 ++ ...resumes-it-only-for-the-session-owner.json | 32 ++ .../agents/n8n-chat-production.api.test.ts | 214 +++++++++ .../agents/n8n-chat-production.model.test.ts | 439 ++++++++++++++++++ .../repositories/agent.repository.test.ts | 42 ++ .../agents/__tests__/useAgentApi.test.ts | 2 + .../agents/composables/useAgentApi.ts | 7 +- .../composables/useAgentBuilderTelemetry.ts | 4 +- packages/workflow/src/interfaces.ts | 2 + 59 files changed, 2359 insertions(+), 108 deletions(-) create mode 100644 packages/cli/test/integration/agents/cassettes/n8n-chat-production/allows-only-the-session-owner-to-cancel-a-suspended-run.json create mode 100644 packages/cli/test/integration/agents/cassettes/n8n-chat-production/sends-an-image-to-the-model-and-reloads-the-stored-attachment.json create mode 100644 packages/cli/test/integration/agents/cassettes/n8n-chat-production/streams-the-published-instructions-and-records-a-private-production-session.json create mode 100644 packages/cli/test/integration/agents/cassettes/n8n-chat-production/suspends-a-tool-call-and-resumes-it-only-for-the-session-owner.json create mode 100644 packages/cli/test/integration/agents/n8n-chat-production.api.test.ts create mode 100644 packages/cli/test/integration/agents/n8n-chat-production.model.test.ts diff --git a/biome.jsonc b/biome.jsonc index 9c7db97acb2..e09bc4a3683 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -34,6 +34,8 @@ "**/*.vue", // Generated DB schema docs "docs/generated/**", + // Recorded model responses + "**/cassettes/n8n-chat-production/**", // Generated / vendored test-impact data ".github/test-metrics/**", // Vendored SPDX license data and overrides diff --git a/packages/@n8n/api-types/src/agents/__tests__/agent-integration.schema.test.ts b/packages/@n8n/api-types/src/agents/__tests__/agent-integration.schema.test.ts index a967ce68d5d..76c24a1c70d 100644 --- a/packages/@n8n/api-types/src/agents/__tests__/agent-integration.schema.test.ts +++ b/packages/@n8n/api-types/src/agents/__tests__/agent-integration.schema.test.ts @@ -1,6 +1,25 @@ -import { AgentIntegrationSchema } from '../agent-integration.schema'; +import { AgentIntegrationConfigSchema, AgentIntegrationSchema } from '../agent-integration.schema'; +import { AgentJsonConfigSchema } from '../agent-json-config.schema'; describe('AgentIntegrationSchema', () => { + it('accepts n8n Chat without a credential in agent configuration', () => { + expect(AgentIntegrationConfigSchema.parse({ type: 'n8n_chat' })).toEqual({ + type: 'n8n_chat', + credentialId: '', + }); + expect(AgentIntegrationSchema.safeParse({ type: 'n8n_chat' }).success).toBe(false); + }); + + it('rejects duplicate n8n Chat channel entries', () => { + expect( + AgentJsonConfigSchema.safeParse({ + name: 'Agent', + model: 'openai/gpt-4o-mini', + instructions: 'Help', + integrations: [{ type: 'n8n_chat' }, { type: 'n8n_chat' }], + }).success, + ).toBe(false); + }); it('accepts a telegram integration with credential id', () => { const result = AgentIntegrationSchema.safeParse({ type: 'telegram', diff --git a/packages/@n8n/api-types/src/agents/agent-config-lifecycle.ts b/packages/@n8n/api-types/src/agents/agent-config-lifecycle.ts index 7f0a55ad4da..f9410417703 100644 --- a/packages/@n8n/api-types/src/agents/agent-config-lifecycle.ts +++ b/packages/@n8n/api-types/src/agents/agent-config-lifecycle.ts @@ -1,7 +1,7 @@ /** * Draft-lifecycle predicates. A capability is a "draft" while its setup is * pending: the agent config until a model is chosen (`model: ""`), an - * integration entry until a credential is connected (`credentialId: ""`). + * credential-backed integration entry until a credential is connected. */ /** True while no model has been chosen yet (setup pending). */ @@ -10,6 +10,6 @@ export function isDraftAgentConfig(config: { model?: string } | null | undefined } /** True while no credential is connected yet (setup pending). */ -export function isDraftIntegration(integration: { credentialId: string }): boolean { - return integration.credentialId.trim() === ''; +export function isDraftIntegration(integration: { type: string; credentialId: string }): boolean { + return integration.type !== 'n8n_chat' && integration.credentialId.trim() === ''; } diff --git a/packages/@n8n/api-types/src/agents/agent-integration.schema.ts b/packages/@n8n/api-types/src/agents/agent-integration.schema.ts index 5af99d2ebc4..887878a014f 100644 --- a/packages/@n8n/api-types/src/agents/agent-integration.schema.ts +++ b/packages/@n8n/api-types/src/agents/agent-integration.schema.ts @@ -214,12 +214,30 @@ const draftCredentialIntegrations = [ }), ] as const; +const n8nChatIntegration = z + .object({ + type: z.literal('n8n_chat'), + credentialId: z.literal('').default(''), + }) + .strict(); + export const AgentIntegrationSchema = z.discriminatedUnion('type', credentialIntegrations); /** Draft config variant that allows cleared stale credential IDs. */ -export const AgentIntegrationConfigSchema = z.discriminatedUnion( - 'type', - draftCredentialIntegrations, -); +export const AgentIntegrationConfigSchema = z.discriminatedUnion('type', [ + ...draftCredentialIntegrations, + n8nChatIntegration, +]); export type AgentIntegrationConfig = z.infer; + +export type CredentialAgentIntegrationConfig = Exclude< + AgentIntegrationConfig, + { type: 'n8n_chat' } +>; + +export function isCredentialAgentIntegration( + integration: AgentIntegrationConfig, +): integration is CredentialAgentIntegrationConfig { + return integration.type !== 'n8n_chat'; +} diff --git a/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts b/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts index b97c50c843b..7f4b1580371 100644 --- a/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts +++ b/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts @@ -482,7 +482,15 @@ export const AgentJsonConfigBaseSchema = z.object({ .optional(), tasks: z.array(AgentJsonTaskConfigSchema).optional(), providerTools: z.record(z.record(z.unknown())).optional(), - integrations: z.array(AgentIntegrationConfigSchema).optional(), + integrations: z + .array(AgentIntegrationConfigSchema) + .refine( + (integrations) => integrations.filter((entry) => entry.type === 'n8n_chat').length <= 1, + { + message: 'Only one n8n Chat channel is allowed', + }, + ) + .optional(), mcpServers: z .array(McpServerConfigSchema) .max(20) diff --git a/packages/@n8n/api-types/src/agents/dto.ts b/packages/@n8n/api-types/src/agents/dto.ts index 32a050bd6c3..b62c14ec086 100644 --- a/packages/@n8n/api-types/src/agents/dto.ts +++ b/packages/@n8n/api-types/src/agents/dto.ts @@ -40,6 +40,7 @@ export const AGENT_SESSION_ORIGINS = [ 'sub-agent', 'schedule', 'workflow', + 'n8n_chat_production', 'slack', 'telegram', 'linear', diff --git a/packages/@n8n/api-types/src/agents/types.ts b/packages/@n8n/api-types/src/agents/types.ts index 3353563a078..ddb56e05254 100644 --- a/packages/@n8n/api-types/src/agents/types.ts +++ b/packages/@n8n/api-types/src/agents/types.ts @@ -164,6 +164,7 @@ export interface AgentIntegrationStatusResponse { */ status: 'configured' | 'connected' | 'disconnected' | 'partial' | 'error'; integrations: AgentIntegrationStatusEntry[]; + n8nChat: { draftEnabled: boolean; publishedEnabled: boolean }; } export interface AgentDisconnectIntegrationResponse { @@ -408,8 +409,8 @@ export interface AgentSessionLangSmithExportResponse { } /** - * Internal integration type for the in-app chat channel. Injected per-run for - * `/chat` executions — never persisted in an agent's `integrations` array. + * Integration type for n8n Chat. Preview injects its tools per run. A configured + * channel entry enables production chat after publish. */ export const N8N_CHAT_INTEGRATION_TYPE = 'n8n_chat' as const; /** Fixed tool names for the implicit in-app chat integration (no credential suffixes). */ diff --git a/packages/cli/src/modules/agents/__tests__/agent-chat-execution.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-chat-execution.service.test.ts index b71a8c06b28..e68ae2013d4 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-chat-execution.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-chat-execution.service.test.ts @@ -228,3 +228,51 @@ it('leaves a checkpoint claimed by a later execution intact during cleanup', asy expect(checkpointStorage.cancelSuspended).not.toHaveBeenCalled(); expect(checkpointStorage.delete).not.toHaveBeenCalled(); }); + +it('requires the production source before cancelling a suspended run', async () => { + const { service, executionService, checkpointStorage } = makeService(); + checkpointStorage.getStatus.mockResolvedValue({ + status: 'active', + checkpoint: { + ...checkpoint, + persistence: { + threadId: context.threadId, + resourceId: 'n8n-chat-production:user-1', + }, + }, + }); + executionService.canUseProductionChatThread.mockResolvedValue(false); + expect( + await service.cancelSuspended({ + agentId: context.agentId, + runId: 'run-1', + resourceId: 'n8n-chat-production:user-1', + }), + ).toBe(false); + expect(checkpointStorage.cancelSuspended).not.toHaveBeenCalled(); + executionService.canUseProductionChatThread.mockResolvedValue(true); + expect( + await service.cancelSuspended({ + agentId: context.agentId, + runId: 'run-1', + resourceId: 'n8n-chat-production:user-1', + }), + ).toBe(true); + expect(executionService.canUseProductionChatThread).toHaveBeenCalledWith( + context.threadId, + context.projectId, + context.agentId, + context.userId, + 'existing', + ); +}); + +it('rejects preview and foreign executions on the production cancel route', async () => { + const { service, repository } = makeService(); + const production = { ...context, productionN8nChat: true }; + await expect(service.requestCancel(production)).rejects.toBeInstanceOf(NotFoundError); + repository.findOneBy.mockResolvedValue({ ...running, source: 'n8n_chat_production' }); + await expect( + service.requestCancel({ ...production, userId: 'other-user' }), + ).rejects.toBeInstanceOf(NotFoundError); +}); diff --git a/packages/cli/src/modules/agents/__tests__/agent-chat.controller.test.ts b/packages/cli/src/modules/agents/__tests__/agent-chat.controller.test.ts index d3271379d5a..d978142e848 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-chat.controller.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-chat.controller.test.ts @@ -40,7 +40,12 @@ import { function makeController() { const agentsService = - mock>(); + mock< + Pick< + AgentsService, + 'findById' | 'findByProjectId' | 'findByProjectIdPaginated' | 'isN8nChatPublished' + > + >(); const agentExecutionOrchestratorService = mock(); const agentsBuilderService = mock(); const agentChatAttachmentService = mock(); @@ -59,6 +64,7 @@ function makeController() { const chatExecutionService = mock(); agentExecutionService.findThreadById.mockResolvedValue(null); agentExecutionService.canUseDraftThread.mockResolvedValue(true); + agentExecutionService.canUseProductionChatThread.mockResolvedValue(true); agentValidationService.validateAgentIsRunnable.mockResolvedValue({ missing: [] }); const agentTestRunService = new AgentTestRunService( agentExecutionService, @@ -108,9 +114,10 @@ function makeController() { agentChatAttachmentService, agentsService: { findById: agentsService.findById, + isN8nChatPublished: agentsService.isN8nChatPublished, getConversationHistory: agentExecutionOrchestratorService.getConversationHistory, } as Mocked< - Pick & + Pick & Pick >, }; @@ -153,6 +160,12 @@ describe('AgentChatController route access scopes', () => { const routes = getRoutesByHandlerName(AgentChatController); it.each([ + ['productionChat', 'agent:execute'], + ['productionChatResume', 'agent:execute'], + ['cancelProductionChatExecution', 'agent:execute'], + ['cancelProductionChatRun', 'agent:execute'], + ['getProductionChatMessages', 'agent:read'], + ['getProductionChatAttachment', 'agent:read'], ['chat', 'agent:execute'], ['chatResume', 'agent:execute'], ['cancelChatRun', 'agent:execute'], @@ -942,3 +955,118 @@ describe('AgentChatController attachment download', () => { expect((res as { setHeader: ReturnType }).setHeader).not.toHaveBeenCalled(); }); }); + +describe('AgentChatController production n8n Chat', () => { + const request = { params: { projectId: 'project-1' }, user: { id: 'user-1' } }; + + it('rejects an unpublished channel before starting a turn', async () => { + const { controller, agentsService, agentExecutionOrchestratorService } = makeController(); + agentsService.isN8nChatPublished.mockResolvedValue(false); + const writes: string[] = []; + await controller.productionChat(request as never, makeSseResponse(writes), 'agent-1', { + message: 'hello', + } as never); + expect(writes).toContain( + 'data: {"type":"error","message":"This agent is not available in n8n Chat.","errorCode":"agent_unavailable"}\n\n', + ); + expect(agentExecutionOrchestratorService.executeForN8nChatPublished).not.toHaveBeenCalled(); + }); + + it('records a published turn with a user-owned memory resource', async () => { + const { controller, agentsService, agentExecutionOrchestratorService } = makeController(); + agentsService.isN8nChatPublished.mockResolvedValue(true); + agentExecutionOrchestratorService.executeForN8nChatPublished.mockImplementation( + async function* () { + yield { type: 'text-delta', id: 'text-1', delta: 'Hi' }; + }, + ); + const writes: string[] = []; + await controller.productionChat(request as never, makeSseResponse(writes), 'agent-1', { + message: 'hello', + } as never); + expect(agentExecutionOrchestratorService.executeForN8nChatPublished).toHaveBeenCalledWith( + expect.objectContaining({ + agentId: 'agent-1', + message: 'hello', + sessionMode: 'new', + memory: expect.objectContaining({ resourceId: 'n8n-chat-production:user-1' }), + }), + ); + expect(writes.some((line) => line.includes('"delta":"Hi"'))).toBe(true); + }); + + it('rejects a foreign session before saving an attachment', async () => { + const { controller, agentsService, agentExecutionService, agentChatAttachmentService } = + makeController(); + agentsService.isN8nChatPublished.mockResolvedValue(true); + agentExecutionService.canUseProductionChatThread.mockResolvedValue(false); + const writes: string[] = []; + await controller.productionChat(request as never, makeSseResponse(writes), 'agent-1', { + message: 'hello', + sessionId: 'foreign-thread', + attachments: [ + { + fileName: 'note.txt', + mimeType: 'text/plain', + data: Buffer.from('hi').toString('base64'), + }, + ], + } as never); + expect(agentChatAttachmentService.storeInbound).not.toHaveBeenCalled(); + expect(writes.some((line) => line.includes('Session not found'))).toBe(true); + }); + + it('checks publication and the production checkpoint scope before resuming', async () => { + const { controller, agentsService, agentExecutionOrchestratorService } = makeController(); + agentsService.isN8nChatPublished.mockResolvedValue(true); + agentExecutionOrchestratorService.resumeForChat.mockImplementation(async function* () {}); + await controller.productionChatResume(request as never, makeSseResponse([]), 'agent-1', { + runId: 'run-1', + toolCallId: 'call-1', + resumeData: { approved: true }, + } as never); + expect(agentExecutionOrchestratorService.resumeForChat).toHaveBeenCalledWith( + expect.objectContaining({ + usePublishedVersion: true, + source: 'n8n_chat_production', + expectedMemory: { resourceId: 'n8n-chat-production:user-1' }, + }), + ); + }); + + it('rejects production resume when the channel is not published', async () => { + const { controller, agentsService, agentExecutionOrchestratorService } = makeController(); + agentsService.isN8nChatPublished.mockResolvedValue(false); + const writes: string[] = []; + await controller.productionChatResume(request as never, makeSseResponse(writes), 'agent-1', { + runId: 'run-1', + toolCallId: 'call-1', + resumeData: { approved: true }, + } as never); + + expect(agentExecutionOrchestratorService.resumeForChat).not.toHaveBeenCalled(); + expect(writes.some((line) => line.includes('agent_unavailable'))).toBe(true); + }); + + it('does not finish a turn that suspended for HITL', async () => { + const { controller, agentsService, agentExecutionOrchestratorService } = makeController(); + agentsService.isN8nChatPublished.mockResolvedValue(true); + agentExecutionOrchestratorService.executeForN8nChatPublished.mockImplementation( + async function* () { + yield { + type: 'tool-call-suspended', + toolCallId: 'call-1', + toolName: 'ask_questions', + runId: 'run-1', + suspendPayload: { type: 'questions', questions: [] }, + }; + }, + ); + const writes: string[] = []; + await controller.productionChat(request as never, makeSseResponse(writes), 'agent-1', { + message: 'Ask me first', + } as never); + expect(writes.some((line) => line.includes('"type":"tool-call-suspended"'))).toBe(true); + expect(writes.some((line) => line.includes('"type":"done"'))).toBe(false); + }); +}); diff --git a/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts index cc93b1053a7..5d373836940 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts @@ -424,6 +424,29 @@ describe('AgentConfigService', () => { expect(runtimeCacheService.clearRuntimes).toHaveBeenCalledWith(agentId); }); + it('persists n8n Chat as a draft channel', async () => { + const { service, agentRepository } = makeService(); + const agent = makeAgent({ integrations: [{ type: 'slack', credentialId: 'slack-cred' }] }); + agentRepository.findByIdAndProjectId.mockResolvedValue(agent); + await service.updateConfig( + agentId, + projectId, + { + ...baseConfig, + integrations: [{ type: 'slack', credentialId: 'slack-cred' }, { type: 'n8n_chat' }], + }, + user, + fencedOn(agent), + ); + expect(agent.integrations).toEqual([ + { type: 'slack', credentialId: '' }, + { type: 'n8n_chat', credentialId: '' }, + ]); + const saved = agentRepository.saveDraftFenced.mock.calls.at(-1)?.[0]; + expect(saved?.integrations).toEqual(agent.integrations); + expect(composeJsonConfig(agent)?.integrations).toEqual(agent.integrations); + }); + it('persists modelDeploymentName, retains it when omitted, and drops it on clearOmittedOptionalFields', async () => { const { service, agentRepository } = makeService(); const agent = makeAgent(); diff --git a/packages/cli/src/modules/agents/__tests__/agent-execution-orchestrator.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-execution-orchestrator.service.test.ts index b329b98172e..3f58a2bd656 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-execution-orchestrator.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-execution-orchestrator.service.test.ts @@ -1440,6 +1440,124 @@ describe('AgentExecutionOrchestratorService', () => { ); }); + it('runs production n8n Chat with a published user-owned session', async () => { + const { + service, + agentRepository, + runtimeCacheService, + executionService, + integrationMessageContextService, + } = makeService(); + agentRepository.isN8nChatPublished.mockResolvedValue(true); + executionService.canUseProductionChatThread.mockResolvedValue(true); + runtimeCacheService.getRuntime.mockResolvedValue(makeRuntime()); + + await collect( + service.executeForN8nChatPublished({ + agentId, + projectId, + user, + message: 'Hello', + memory: { threadId: 'thread-1', resourceId: 'n8n-chat-production:user-1' }, + }), + ); + + expect(runtimeCacheService.getRuntime).toHaveBeenCalledWith( + expect.objectContaining({ + integrationType: N8N_CHAT_INTEGRATION_TYPE, + usePublishedVersion: true, + allowBackgroundTasks: false, + attributionUserId: user.id, + }), + ); + expect(integrationMessageContextService.setLatest).toHaveBeenCalledWith( + 'thread-1', + 'n8n-chat-production:user-1', + expect.objectContaining({ + platform: N8N_CHAT_INTEGRATION_TYPE, + interactingUserId: user.id, + target: { type: 'dm', userId: user.id, threadId: 'thread-1' }, + }), + ); + expect(executionService.startExecutionRecording).toHaveBeenCalledWith( + expect.objectContaining({ + access: { accessScope: 'user', ownerId: user.id }, + source: 'n8n_chat_production', + telemetry: expect.objectContaining({ runType: 'production', userId: user.id }), + }), + expect.any(Date), + ); + }); + + it('rejects a production turn with a foreign thread or memory scope', async () => { + const { service, agentRepository, runtimeCacheService, executionService } = makeService(); + agentRepository.isN8nChatPublished.mockResolvedValue(true); + for (const resourceId of ['draft-chat:user-1', 'n8n-chat-production:other-user']) { + await expect( + collect( + service.executeForN8nChatPublished({ + agentId, + projectId, + user, + message: 'Hello', + memory: { threadId: 'thread-1', resourceId }, + }), + ), + ).rejects.toThrow('Session not found'); + } + executionService.canUseProductionChatThread.mockResolvedValue(false); + await expect( + collect( + service.executeForN8nChatPublished({ + agentId, + projectId, + user, + message: 'Hello', + memory: { threadId: 'thread-1', resourceId: 'n8n-chat-production:user-1' }, + }), + ), + ).rejects.toThrow('Session not found'); + expect(runtimeCacheService.getRuntime).not.toHaveBeenCalled(); + }); + + it('rejects a production resume with a different sandbox principal', async () => { + const { service, agentRepository, checkpointStorage, executionService, runtimeCacheService } = + makeService(); + agentRepository.isN8nChatPublished.mockResolvedValue(true); + executionService.canUseProductionChatThread.mockResolvedValue(true); + checkpointStorage.getStatus.mockResolvedValue({ + status: 'active', + checkpoint: makeCheckpoint( + {}, + { + threadId: 'thread-1', + resourceId: 'n8n-chat-production:user-1', + hostMetadata: encodeAgentSandboxHostMetadata({ + projectId, + principalHash: integrationPrincipalHash, + }), + }, + ), + }); + + await expect( + collect( + service.resumeForChat({ + agentId, + projectId, + user, + runId: 'run-1', + toolCallId: 'call-1', + resumeData: { approved: true }, + usePublishedVersion: true, + integrationType: N8N_CHAT_INTEGRATION_TYPE, + source: 'n8n_chat_production', + }), + ), + ).rejects.toThrow('unavailable'); + expect(runtimeCacheService.getRuntime).not.toHaveBeenCalled(); + }); + it.each(['none', 'startExecutionRecording', 'finalizeExecution'] as const)( 'reports preview initialization failures with the recording outcome (%s)', async (failure) => { @@ -2044,6 +2162,50 @@ describe('AgentExecutionOrchestratorService', () => { ).toEqual(messageContext); }); + it('records a production n8n Chat wake in the owning user session', async () => { + const { + service, + agentRepository, + executionService, + runtimeCacheService, + chatIntegrationService, + } = makeService(); + const runtime = makeRuntime(); + agentRepository.isN8nChatPublished.mockResolvedValue(true); + executionService.canUseProductionChatThread.mockResolvedValue(true); + runtimeCacheService.getRuntime.mockResolvedValue(runtime); + + await service.executeForWake({ + backgroundJobSignal, + agentId, + projectId, + message: 'The job is done.', + memory: { threadId: 'thread-1', resourceId: 'n8n-chat-production:user-1' }, + identity: { + type: 'published', + integrationType: N8N_CHAT_INTEGRATION_TYPE, + principalHash: userPrincipalHash, + }, + abortSignal: new AbortController().signal, + }); + + expect(runtimeCacheService.getRuntime).toHaveBeenCalledWith( + expect.objectContaining({ + integrationType: N8N_CHAT_INTEGRATION_TYPE, + usePublishedVersion: true, + attributionUserId: userId, + }), + ); + expect(executionService.startExecutionRecording).toHaveBeenCalledWith( + expect.objectContaining({ + source: 'n8n_chat_production', + access: { accessScope: 'user', ownerId: userId }, + }), + expect.any(Date), + ); + expect(chatIntegrationService.getBridge).not.toHaveBeenCalled(); + }); + it('rejects a wake when chat delivery fails and releases the runtime', async () => { const { service, executionService, runtimeCacheService, bridge } = makeService(); const runtime = makeRuntime(); diff --git a/packages/cli/src/modules/agents/__tests__/agent-execution.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-execution.service.test.ts index fc6d477e98e..c779b82cdc5 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-execution.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-execution.service.test.ts @@ -1240,6 +1240,53 @@ describe('AgentExecutionService', () => { }); }); + describe('canUseProductionChatThread', () => { + it.each([ + { source: 'n8n_chat_production', allowed: true }, + { source: 'n8n_chat', allowed: false }, + { source: 'chat', allowed: false }, + ])('checks the first source: $source', async ({ source, allowed }) => { + agentExecutionThreadRepository.findOneBy.mockResolvedValue(makeThread()); + agentExecutionRepository.findFirstSourceByThreadIds.mockResolvedValue( + new Map([['thread-1', source]]), + ); + expect( + await service.canUseProductionChatThread( + 'thread-1', + 'project-1', + 'agent-1', + 'user-1', + 'existing', + ), + ).toBe(allowed); + }); + + it('rejects another owner and an unknown existing session', async () => { + agentExecutionThreadRepository.findOneBy.mockResolvedValue( + makeThread({ ownerId: 'other-user' }), + ); + expect( + await service.canUseProductionChatThread( + 'thread-1', + 'project-1', + 'agent-1', + 'user-1', + 'existing', + ), + ).toBe(false); + agentExecutionThreadRepository.findOneBy.mockResolvedValue(null); + expect( + await service.canUseProductionChatThread( + 'thread-1', + 'project-1', + 'agent-1', + 'user-1', + 'existing', + ), + ).toBe(false); + }); + }); + describe('getThreadDetail', () => { it.each(['user', 'project'] as const)( 'returns readable %s thread executions', diff --git a/packages/cli/src/modules/agents/__tests__/agent-integrations.controller.test.ts b/packages/cli/src/modules/agents/__tests__/agent-integrations.controller.test.ts index bd5d0c3f37a..e0af76b678d 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-integrations.controller.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-integrations.controller.test.ts @@ -469,6 +469,39 @@ describe('AgentIntegrationsController channel status', () => { ]); }); + it('reports draft and active n8n Chat availability separately', async () => { + const agent = { + ...publishedAgent, + activeVersion: { + schema: { + name: 'Agent', + model: 'openai:gpt-4o-mini', + instructions: 'Help', + integrations: [{ type: 'n8n_chat', credentialId: '' }], + }, + }, + } as Agent; + const { response } = await statusOf(agent, []); + expect(response.n8nChat).toEqual({ draftEnabled: false, publishedEnabled: true }); + expect(response.integrations).toEqual([ + { type: slack.type, credentialId: slack.credentialId, status: 'starting' }, + { type: telegram.type, credentialId: telegram.credentialId, status: 'starting' }, + { type: 'n8n_chat', status: 'connected' }, + ]); + const unpublished = { + ...agent, + activeVersionId: null, + activeVersion: null, + integrations: [...agent.integrations, { type: 'n8n_chat', credentialId: '' }], + } as Agent; + const { response: unpublishedResponse } = await statusOf(unpublished, []); + expect(unpublishedResponse.n8nChat).toEqual({ draftEnabled: true, publishedEnabled: false }); + expect(unpublishedResponse.integrations).toContainEqual({ + type: 'n8n_chat', + status: 'configured', + }); + }); + it('reports the reason a channel could not start', async () => { const failed = { ...liveRow(telegram), diff --git a/packages/cli/src/modules/agents/__tests__/agent-publish.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-publish.service.test.ts index 0151761282d..a2e58130365 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-publish.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-publish.service.test.ts @@ -404,7 +404,7 @@ describe('AgentPublishService', () => { tasks: [{ type: 'task', id: 'task-1', enabled: true }], }, skills: configuredSkills, - integrations, + integrations: [...integrations, { type: 'n8n_chat', credentialId: '' }], }); const draftValidation = { status: 'valid' as const, issues: [] }; const task = { @@ -435,7 +435,7 @@ describe('AgentPublishService', () => { { versionId, agentId, - schema: agent.schema, + schema: { ...agent.schema, integrations: [{ type: 'n8n_chat', credentialId: '' }] }, tools: configuredTools, skills: configuredSkills, publishedBy: user, @@ -605,8 +605,12 @@ describe('AgentPublishService', () => { it('switches to an existing history row when publishing a specific version', async () => { const { service, agentRepository, agentHistoryRepository, trx } = makeService(); - const agent = makeAgent({ versionId: 'draft-v2', activeVersionId: 'v0' }); - const target = makeHistory({ versionId: 'v1' }); + const agent = makeAgent({ + versionId: 'draft-v2', + activeVersionId: 'v0', + integrations: [{ type: 'n8n_chat', credentialId: '' }], + }); + const target = makeHistory({ versionId: 'v1', schema: { ...schema, integrations: [] } }); agentRepository.findByIdAndProjectId.mockResolvedValue(agent); agentHistoryRepository.findByVersionAndAgentId.mockResolvedValue(target); @@ -619,6 +623,9 @@ describe('AgentPublishService', () => { expect(agentHistoryRepository.findByVersionAndAgentId).toHaveBeenCalledWith('v1', agentId); expect(agent.activeVersionId).toBe('v1'); expect(agent.activeVersion).toBe(target); + expect(agent.integrations).toEqual([{ type: 'n8n_chat', credentialId: '' }]); + expect(target.schema?.integrations).toEqual([]); + expect(agentHistoryRepository.saveVersion).not.toHaveBeenCalled(); expect(agent.versionId).not.toBe('draft-v2'); expect(agentRepository.setActiveVersionFenced).toHaveBeenCalledWith( agent.id, @@ -706,10 +713,11 @@ describe('AgentPublishService', () => { const agent = makeAgent({ activeVersionId: 'current-active', activeVersion: makeHistory({ versionId: 'current-active' }), + integrations: [{ type: 'n8n_chat', credentialId: '' }], }); const target = makeHistory({ versionId: 'older-version', - schema: { ...schema, name: 'Older Agent' }, + schema: { ...schema, name: 'Older Agent', integrations: [] }, }); agentRepository.findByIdAndProjectId.mockResolvedValue(agent); @@ -726,9 +734,10 @@ describe('AgentPublishService', () => { await service.revertToVersion(agentId, projectId, 'older-version', user, 'user'); - expect(agent.schema).toEqual(target.schema); + expect(agent.schema).toEqual({ ...schema, name: 'Older Agent' }); expect(agent.name).toBe('Older Agent'); expect(agent.activeVersionId).toBe('current-active'); + expect(agent.integrations).toEqual([{ type: 'n8n_chat', credentialId: '' }]); expect(agent.versionId).not.toBe('older-version'); expect(taskRepo.delete).toHaveBeenCalledWith(['draft-only']); expect(taskRepo.update).toHaveBeenCalledWith( diff --git a/packages/cli/src/modules/agents/__tests__/agent-runtime-cache.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-runtime-cache.service.test.ts index d6a766e6479..229d4caa8ec 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-runtime-cache.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-runtime-cache.service.test.ts @@ -624,6 +624,51 @@ describe('AgentRuntimeCacheService', () => { ); }); + it('keeps production n8n Chat on published instructions with project-scoped tools', async () => { + const { service, agentRepository, reconstructionService } = makeService(); + const agent = makeAgent({ + schema: { + name: 'Draft', + model: 'openai:gpt-4o', + instructions: 'Draft instructions', + }, + activeVersion: { + schema: { + name: 'Published', + model: 'openai:gpt-4o', + instructions: 'Published instructions', + }, + tools: {}, + skills: {}, + } as Agent['activeVersion'], + }); + agentRepository.findByIdAndProjectId.mockResolvedValue(agent); + reconstructionService.reconstructFromAgentEntity.mockResolvedValue(makeRuntime()); + + await service.getRuntime({ + agentId, + projectId, + usePublishedVersion: true, + integrationType: 'n8n_chat', + attributionUserId: 'user-1', + allowBackgroundTasks: false, + }); + + expect(reconstructionService.reconstructFromAgentEntity).toHaveBeenCalledWith( + expect.objectContaining({ + schema: expect.objectContaining({ instructions: 'Published instructions' }), + }), + expect.anything(), + 'production', + 'n8n_chat', + undefined, + undefined, + 'integrated', + undefined, + { previewChat: undefined, allowBackgroundTasks: false, attributionUserId: 'user-1' }, + ); + }); + it('rejects missing agents and unpublished runtime requests', async () => { const { service, agentRepository } = makeService(); diff --git a/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-tool-filtering.test.ts b/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-tool-filtering.test.ts index ef11f7215ca..404332ba43e 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-tool-filtering.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-tool-filtering.test.ts @@ -1,9 +1,19 @@ import type * as agents from '@n8n/agents'; import type { CredentialProvider } from '@n8n/agents'; -import type { AgentJsonConfig, AgentJsonToolConfig } from '@n8n/api-types'; +import { + N8N_CHAT_INTEGRATION_TYPE, + type AgentJsonConfig, + type AgentJsonToolConfig, +} from '@n8n/api-types'; import type { Logger } from '@n8n/backend-common'; import type { CustomFetch, HttpTransport, OutboundHttp } from '@n8n/backend-network'; -import type { CredentialsEntity, User, WorkflowEntity, WorkflowRepository } from '@n8n/db'; +import type { + CredentialsEntity, + User, + UserRepository, + WorkflowEntity, + WorkflowRepository, +} from '@n8n/db'; import { Container } from '@n8n/di'; import { mock } from 'vitest-mock-extended'; @@ -23,8 +33,13 @@ import { AgentRuntimeReconstructionService } from '../agent-runtime-reconstructi import type { AgentSandboxRuntimeService } from '../agent-sandbox-runtime.service'; import type { AgentWorkspaceService } from '../agent-workspace.service'; import type { Agent } from '../entities/agent.entity'; +import { ChatIntegrationRegistry } from '../integrations/agent-chat-integration'; +import { ChatIntegrationActionExecutor } from '../integrations/integration-action-executor'; +import { ChatIntegrationContextQueryExecutor } from '../integrations/integration-context-query-executor'; +import { IntegrationMessageContextService } from '../integrations/integration-message-context.service'; import type { N8NCheckpointStorage } from '../integrations/n8n-checkpoint-storage'; import type { N8nMemory } from '../integrations/n8n-memory'; +import { N8nChatIntegration } from '../integrations/platforms/n8n-chat-integration'; import type * as FromJsonConfig from '../json-config/from-json-config'; import type { BuildFromJsonOptions, ToolExecutor } from '../json-config/from-json-config'; import type { AgentFileRepository } from '../repositories/agent-file.repository'; @@ -175,6 +190,15 @@ function buildFromJsonResolvingTools(resolved: Array())); + Container.set(ChatIntegrationRegistry, registry); + Container.set(IntegrationMessageContextService, mock()); + Container.set(ChatIntegrationActionExecutor, mock()); + Container.set(ChatIntegrationContextQueryExecutor, mock()); +} + function toolNamesPassedToBuildFromJson(): string[] { const [config] = buildFromJsonMock.mock.calls.at(-1) as [AgentJsonConfig]; return (config.tools ?? []).map((ref) => @@ -206,6 +230,54 @@ describe('AgentRuntimeReconstructionService — per-user tool filtering', () => ); }); + it('forwards the production n8n Chat marker to workflow tools', async () => { + const { service } = makeService({}); + setupN8nChatToolDependencies(); + buildFromJsonResolvingTools([]); + await service.reconstructFromAgentEntity( + makeAgentEntity([workflowTool]), + mock(), + 'production', + N8N_CHAT_INTEGRATION_TYPE, + undefined, + undefined, + 'manual', + undefined, + { attributionUserId: userId }, + ); + + expect(resolveWorkflowToolMock).toHaveBeenCalledWith( + workflowTool, + expect.objectContaining({ + integrationType: N8N_CHAT_INTEGRATION_TYPE, + userId, + publishedN8nChat: true, + }), + ); + }); + + it('forwards the preview marker to workflow tools', async () => { + const { service } = makeService({}); + setupN8nChatToolDependencies(); + buildFromJsonResolvingTools([]); + await service.reconstructFromAgentEntity( + makeAgentEntity([workflowTool]), + mock(), + 'test', + N8N_CHAT_INTEGRATION_TYPE, + undefined, + undefined, + 'manual', + undefined, + { previewChat: true }, + ); + + expect(resolveWorkflowToolMock).toHaveBeenCalledWith( + workflowTool, + expect.objectContaining({ previewChat: true, publishedN8nChat: false }), + ); + }); + it('drops node and workflow tools for a user without workflow:execute, keeps custom tools', async () => { vi.mocked(userHasScopes).mockResolvedValue(false); const { service } = makeService({}); diff --git a/packages/cli/src/modules/agents/__tests__/agent-validation.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-validation.service.test.ts index 26482aac877..c796633d74b 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-validation.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-validation.service.test.ts @@ -897,6 +897,41 @@ describe('AgentValidationService — structured issues', () => { ]); }); + it('accepts credential-free n8n Chat without relaxing other channel checks', async () => { + const { service, agentRepository } = makeService(); + agentRepository.findByIdAndProjectId.mockResolvedValue( + makeAgent(runnableConfig, {}, { integrations: [{ type: 'n8n_chat', credentialId: '' }] }), + ); + const credentials = makeCredentialProvider([{ id: 'openai-main', type: 'openAiApi' }]); + + await expect( + service.validateAgentConfiguration(agentId, projectId, credentials), + ).resolves.toEqual({ status: 'valid', issues: [] }); + + agentRepository.findByIdAndProjectId.mockResolvedValue( + makeAgent( + runnableConfig, + {}, + { + integrations: [ + { type: 'n8n_chat', credentialId: '' }, + { type: 'slack', credentialId: '' }, + ], + }, + ), + ); + const result = await service.validateAgentConfiguration(agentId, projectId, credentials); + + expect(result.status).toBe('invalid'); + expect(result.issues).toEqual([ + expect.objectContaining({ + code: 'missing_credential', + path: 'integrations.1.credentialId', + capability: { kind: 'channel', id: 'slack', index: 1 }, + }), + ]); + }); + it('flags a custom tool without a saved body', async () => { const { service, agentRepository } = makeService(); agentRepository.findByIdAndProjectId.mockResolvedValue( diff --git a/packages/cli/src/modules/agents/__tests__/agent-workflow-tool-resume.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-workflow-tool-resume.service.test.ts index b6daf5e52d9..d67a63658c8 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-workflow-tool-resume.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-workflow-tool-resume.service.test.ts @@ -12,6 +12,8 @@ import { mock } from 'vitest-mock-extended'; import type { Publisher } from '@/scaling/pubsub/publisher.service'; import type { AgentExecutionUpdateBroadcaster } from '../agent-execution-update-broadcaster'; +import type { AgentExecutionOrchestratorService } from '../agent-execution-orchestrator.service'; +import type { AgentRepository } from '../repositories/agent.repository'; import type { AgentTestRunService } from '../agent-test-run.service'; import { AgentWorkflowToolResumeService } from '../agent-workflow-tool-resume.service'; import type { AgentBackgroundJobService } from '../background/agent-background-job.service'; @@ -58,6 +60,8 @@ function setup() { }, } as never); const backgroundJobService = mock(); + const orchestratorService = mock(); + const agentRepository = mock(); const service = new AgentWorkflowToolResumeService( logger, userRepository, @@ -69,6 +73,8 @@ function setup() { instanceSettings, publisher, backgroundJobService, + orchestratorService, + agentRepository, ); return { service, @@ -83,9 +89,42 @@ function setup() { instanceSettings, messageContextService, backgroundJobService, + orchestratorService, + agentRepository, }; } +describe('AgentWorkflowToolResumeService production n8n Chat', () => { + it('resumes the published runtime for the owning user', async () => { + const { service, userRepository, agentRepository, orchestratorService, agentTestRunService } = + setup(); + userRepository.findOneBy.mockResolvedValue({ id: 'user-1' } as User); + agentRepository.isN8nChatPublished.mockResolvedValue(true); + orchestratorService.resumeForChat.mockImplementation(async function* () {}); + await service.resume({ ...previewRun, publishedN8nChat: true }, 'success'); + expect(agentTestRunService.resumeDraftRun).not.toHaveBeenCalled(); + expect(orchestratorService.resumeForChat).toHaveBeenCalledWith( + expect.objectContaining({ + usePublishedVersion: true, + source: 'n8n_chat_production', + user: expect.objectContaining({ id: 'user-1' }), + expectedMemory: { + threadId: previewRun.threadId, + resourceId: 'n8n-chat-production:user-1', + }, + }), + ); + }); + + it('does not resume an unpublished agent', async () => { + const { service, userRepository, agentRepository, orchestratorService } = setup(); + userRepository.findOneBy.mockResolvedValue({ id: 'user-1' } as User); + agentRepository.isN8nChatPublished.mockResolvedValue(false); + await service.resume({ ...previewRun, publishedN8nChat: true }, 'success'); + expect(orchestratorService.resumeForChat).not.toHaveBeenCalled(); + }); +}); + /** A `workflowExecuteAfter` context for a sub-execution carrying an agent marker. */ function afterContext( status: IRun['status'], diff --git a/packages/cli/src/modules/agents/agent-chat-execution.service.ts b/packages/cli/src/modules/agents/agent-chat-execution.service.ts index 1c89f88a1a9..f8575462c71 100644 --- a/packages/cli/src/modules/agents/agent-chat-execution.service.ts +++ b/packages/cli/src/modules/agents/agent-chat-execution.service.ts @@ -13,9 +13,15 @@ import { N8NCheckpointStorage } from './integrations/n8n-checkpoint-storage'; import { AgentExecutionRepository } from './repositories/agent-execution.repository'; import { draftChatMemoryResourceId, + productionChatMemoryResourceId, userIdFromDraftChatMemoryResourceId, + userIdFromProductionChatMemoryResourceId, } from './utils/agent-memory-scope'; -import { canContinueThreadInPreview, threadBelongsTo } from './utils/agent-thread-access'; +import { + canContinueThreadInPreview, + N8N_CHAT_PRODUCTION_SOURCE, + threadBelongsTo, +} from './utils/agent-thread-access'; import { getDelegatedChildCheckpoints } from './utils/delegated-child-checkpoints'; type ExecutionContext = PubSubCommandMap['cancel-agent-chat-execution']; @@ -87,7 +93,9 @@ export class AgentChatExecutionService { await this.cancelSuspended({ agentId: context.agentId, runId: suspendedRunId, - resourceId: draftChatMemoryResourceId(context.userId), + resourceId: context.productionN8nChat + ? productionChatMemoryResourceId(context.userId) + : draftChatMemoryResourceId(context.userId), }); }, ); @@ -162,7 +170,17 @@ export class AgentChatExecutionService { const thread = await this.executionService.findThreadById(threadId); if (!thread || !threadBelongsTo(thread, projectId, agentId, userId)) return null; const execution = await this.executionRepository.findOneBy({ id: executionId, threadId }); - if (!execution || !canContinueThreadInPreview(thread, userId, execution.source)) return null; + if ( + !execution || + (context.productionN8nChat + ? !( + thread.accessScope === 'user' && + thread.ownerId === userId && + execution.source === N8N_CHAT_PRODUCTION_SOURCE + ) + : !canContinueThreadInPreview(thread, userId, execution.source)) + ) + return null; return execution; } @@ -180,7 +198,9 @@ export class AgentChatExecutionService { return await this.cancelSuspended({ agentId: context.agentId, runId: pending.runId, - resourceId: draftChatMemoryResourceId(context.userId), + resourceId: context.productionN8nChat + ? productionChatMemoryResourceId(context.userId) + : draftChatMemoryResourceId(context.userId), }); } @@ -196,7 +216,20 @@ export class AgentChatExecutionService { ) return false; const thread = await this.executionService.findThreadById(checkpoint.persistence.threadId); - const userId = userIdFromDraftChatMemoryResourceId(params.resourceId); + const productionUserId = userIdFromProductionChatMemoryResourceId(params.resourceId); + const userId = userIdFromDraftChatMemoryResourceId(params.resourceId) ?? productionUserId; + if ( + productionUserId && + (!thread || + !(await this.executionService.canUseProductionChatThread( + thread.id, + thread.projectId, + params.agentId, + productionUserId, + 'existing', + ))) + ) + return false; if (thread && (!userId || !threadBelongsTo(thread, thread.projectId, params.agentId, userId))) return false; const childCheckpoints = getDelegatedChildCheckpoints(checkpoint, params.agentId); diff --git a/packages/cli/src/modules/agents/agent-chat.controller.ts b/packages/cli/src/modules/agents/agent-chat.controller.ts index cc3369dce78..208630dccc8 100644 --- a/packages/cli/src/modules/agents/agent-chat.controller.ts +++ b/packages/cli/src/modules/agents/agent-chat.controller.ts @@ -26,6 +26,7 @@ import { sanitizeFilename } from '@n8n/utils/files/sanitize-filename'; import type { Response } from 'express'; import { FileNotFoundError, getHtmlSandboxCSP } from 'n8n-core'; import { pipeline } from 'node:stream/promises'; +import { randomUUID } from 'node:crypto'; import { CredentialsService } from '@/credentials/credentials.service'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @@ -33,8 +34,10 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { AgentsCredentialProvider } from './adapters/agents-credential-provider'; import { AgentChatAttachmentService } from './agent-chat-attachment.service'; +import type { AgentChatAttachment } from './entities/agent-chat-attachment.entity'; import type { StoredAttachmentRef } from './types/agent-chat-attachment'; import { AgentExecutionOrchestratorService } from './agent-execution-orchestrator.service'; +import { AgentExecutionRecordingError } from './agent-execution-recording.error'; import { AgentMessageQueueService } from './agent-message-queue.service'; import { AgentQueuedPreviewStreamService } from './agent-queued-preview-stream.service'; import { @@ -42,7 +45,7 @@ import { AgentTurnAlreadyRunningError, } from './agent-chat-execution.service'; import { AgentExecutionService } from './agent-execution.service'; -import { threadBelongsTo } from './utils/agent-thread-access'; +import { N8N_CHAT_PRODUCTION_SOURCE, threadBelongsTo } from './utils/agent-thread-access'; import { messagesToDto } from './agent-message-mapper'; import { type FlushableResponse, initSseStream } from './agent-sse-stream'; import { AgentTestChatService, chatThreadId } from './agent-test-chat.service'; @@ -52,6 +55,7 @@ import { AgentsBuilderService } from './builder/agents-builder.service'; import { AgentBackgroundJobService } from './background/agent-background-job.service'; import { draftChatMemoryResourceId, + productionChatMemoryResourceId, userIdFromDraftChatMemoryResourceId, } from './utils/agent-memory-scope'; import { resolveInboundMimeType } from './utils/inbound-attachments'; @@ -102,8 +106,9 @@ export class AgentChatController { projectId: string; threadId: string; resourceId: string; + source?: string; }): Promise { - const { attachments, agentId, projectId, threadId, resourceId } = params; + const { attachments, agentId, projectId, threadId, resourceId, source = 'chat' } = params; if (!attachments?.length) return undefined; const stored: StoredAttachmentRef[] = []; @@ -125,7 +130,7 @@ export class AgentChatController { projectId, threadId, resourceId, - source: 'chat', + source, fileName: attachment.fileName, mimeType, data, @@ -145,6 +150,184 @@ export class AgentChatController { return stored; } + private async requireProductionChat(agentId: string, projectId: string): Promise { + if (!(await this.agentsService.isN8nChatPublished(agentId, projectId))) { + throw new NotFoundError('Agent is not available in n8n Chat'); + } + } + + private async requireProductionThread( + threadId: string, + projectId: string, + agentId: string, + userId: string, + ): Promise { + if ( + !(await this.agentExecutionService.canUseProductionChatThread( + threadId, + projectId, + agentId, + userId, + 'existing', + )) + ) + throw new NotFoundError('Session not found'); + } + + @Post('/:agentId/n8n-chat', { usesTemplates: true }) + @ProjectScope('agent:execute') + async productionChat( + req: AuthenticatedRequest<{ projectId: string }>, + res: FlushableResponse, + @Param('agentId') agentId: string, + @Body payload: AgentChatMessageDto, + ) { + const { projectId } = req.params; + const execution = this.createChatExecution(res); + const { send, onChunk, abortSignal } = execution; + let executionId: string | undefined; + // `onExecutionRecorded` only fires once the turn finalizes. A disconnect or an + // error before that leaves the started id as the only way to name the execution. + let startedExecutionId: string | undefined; + const onExecutionStarted = (id: string, sessionId: string) => { + startedExecutionId = id; + execution.onExecutionStarted(id, sessionId); + }; + let storedAttachments: StoredAttachmentRef[] | undefined; + try { + if (!(await this.agentsService.isN8nChatPublished(agentId, projectId))) { + send({ + type: 'error', + message: 'This agent is not available in n8n Chat.', + errorCode: 'agent_unavailable', + }); + return; + } + const sessionMode = payload.sessionId && !payload.newSession ? 'existing' : 'new'; + const threadId = sessionMode === 'existing' ? payload.sessionId : randomUUID(); + if (!threadId) throw new NotFoundError('Session not found'); + if (sessionMode === 'existing') { + await this.requireProductionThread(threadId, projectId, agentId, req.user.id); + } + storedAttachments = await this.storeChatAttachments({ + attachments: payload.attachments, + agentId, + projectId, + threadId, + resourceId: productionChatMemoryResourceId(req.user.id), + source: N8N_CHAT_PRODUCTION_SOURCE, + }); + abortSignal.throwIfAborted(); + const stream = this.agentExecutionOrchestratorService.executeForN8nChatPublished({ + agentId, + projectId, + user: req.user, + message: payload.message, + memory: { threadId, resourceId: productionChatMemoryResourceId(req.user.id) }, + attachments: storedAttachments, + sessionMode, + onExecutionStarted, + onExecutionRecorded: (id) => { + executionId = id; + }, + abortSignal, + }); + let suspended = false; + let failed = false; + for await (const chunk of stream) { + onChunk(chunk); + if (chunk.type === 'tool-call-suspended') suspended = true; + if (chunk.type === 'error') failed = true; + } + executionId ??= startedExecutionId; + if (!suspended && !failed) { + send({ type: 'done', sessionId: threadId, ...(executionId ? { executionId } : {}) }); + } + } catch (error) { + executionId ??= startedExecutionId; + if (error instanceof AgentExecutionRecordingError) executionId ??= error.executionId; + if (!executionId && storedAttachments?.length) { + await this.agentChatAttachmentService + .deleteByIds(storedAttachments.map((ref) => ref.id)) + .catch(() => {}); + } + send({ + type: 'error', + message: error instanceof Error ? error.message : 'Chat failed', + ...(error instanceof AgentTurnAlreadyRunningError + ? { errorCode: 'turn_already_running' } + : {}), + }); + } finally { + execution.close(); + } + } + + @Post('/:agentId/n8n-chat/resume', { usesTemplates: true }) + @ProjectScope('agent:execute') + async productionChatResume( + req: AuthenticatedRequest<{ projectId: string }>, + res: FlushableResponse, + @Param('agentId') agentId: string, + @Body payload: AgentChatResumeDto, + ) { + const execution = this.createChatExecution(res); + const { send, onChunk, abortSignal } = execution; + let startedExecutionId: string | undefined; + const onExecutionStarted = (id: string, sessionId: string) => { + startedExecutionId = id; + execution.onExecutionStarted(id, sessionId); + }; + try { + if (!(await this.agentsService.isN8nChatPublished(agentId, req.params.projectId))) { + send({ + type: 'error', + message: 'This agent is not available in n8n Chat.', + errorCode: 'agent_unavailable', + }); + return; + } + abortSignal.throwIfAborted(); + const stream = this.agentExecutionOrchestratorService.resumeForChat({ + agentId, + projectId: req.params.projectId, + runId: payload.runId, + toolCallId: payload.toolCallId, + resumeData: payload.resumeData, + user: req.user, + usePublishedVersion: true, + integrationType: 'n8n_chat', + source: N8N_CHAT_PRODUCTION_SOURCE, + expectedMemory: { resourceId: productionChatMemoryResourceId(req.user.id) }, + onExecutionStarted, + abortSignal, + }); + let suspended = false; + let failed = false; + for await (const chunk of stream) { + onChunk(chunk); + if (chunk.type === 'tool-call-suspended') suspended = true; + if (chunk.type === 'error') failed = true; + } + if (!suspended && !failed) { + send({ + type: 'done', + ...(startedExecutionId ? { executionId: startedExecutionId } : {}), + }); + } + } catch (error) { + send({ + type: 'error', + message: error instanceof Error ? error.message : 'Resume failed', + ...(error instanceof AgentTurnAlreadyRunningError + ? { errorCode: 'turn_already_running' } + : {}), + }); + } finally { + execution.close(); + } + } + @Post('/:agentId/chat', { usesTemplates: true }) @ProjectScope('agent:execute') async chat( @@ -334,6 +517,102 @@ export class AgentChatController { return { cancelled }; } + @Delete('/:agentId/n8n-chat/:threadId/executions/:executionId') + @ProjectScope('agent:execute') + async cancelProductionChatExecution( + req: AuthenticatedRequest<{ projectId: string }>, + _res: Response, + @Param('agentId') agentId: string, + @Param('threadId') threadId: string, + @Param('executionId') executionId: string, + ) { + await this.requireProductionChat(agentId, req.params.projectId); + await this.requireProductionThread(threadId, req.params.projectId, agentId, req.user.id); + return { + cancelRequested: await this.chatExecutionService.requestCancel({ + projectId: req.params.projectId, + agentId, + threadId, + executionId, + userId: req.user.id, + productionN8nChat: true, + }), + }; + } + + @Delete('/:agentId/n8n-chat/runs/:runId') + @ProjectScope('agent:execute') + async cancelProductionChatRun( + req: AuthenticatedRequest<{ projectId: string }>, + _res: Response, + @Param('agentId') agentId: string, + @Param('runId') runId: string, + ) { + await this.requireProductionChat(agentId, req.params.projectId); + return { + cancelled: await this.agentExecutionOrchestratorService.cancelChatRun({ + agentId, + runId, + resourceId: productionChatMemoryResourceId(req.user.id), + }), + }; + } + + @Get('/:agentId/n8n-chat/:threadId/messages') + @ProjectScope('agent:read') + async getProductionChatMessages( + req: AuthenticatedRequest<{ projectId: string; agentId: string; threadId: string }>, + ): Promise { + const { projectId, agentId, threadId } = req.params; + await this.requireProductionChat(agentId, projectId); + await this.requireProductionThread(threadId, projectId, agentId, req.user.id); + const history = await this.agentExecutionOrchestratorService.getConversationHistory({ + threadId, + projectId, + agentId, + userId: req.user.id, + }); + if (!history) throw new NotFoundError('Session not found'); + const checkpoint = await this.agentsBuilderService.findOpenCheckpointForThread( + agentId, + threadId, + ); + return { + ...withOpenSuspensions( + history.messages, + checkpoint?.persistence?.resourceId === productionChatMemoryResourceId(req.user.id) + ? checkpoint + : null, + { appendInactiveCheckpointMessages: false }, + ), + activeExecutionId: history.activeExecutionId, + }; + } + + @Get('/:agentId/n8n-chat/attachments/:attachmentId') + @ProjectScope('agent:read') + async getProductionChatAttachment( + req: AuthenticatedRequest<{ projectId: string; agentId: string; attachmentId: string }>, + res: Response, + ) { + const { projectId, agentId, attachmentId } = req.params; + await this.requireProductionChat(agentId, projectId); + const attachment = await this.agentChatAttachmentService.getForAgent(attachmentId, { + agentId, + projectId, + userId: req.user.id, + }); + if ( + !attachment || + attachment.source !== N8N_CHAT_PRODUCTION_SOURCE || + attachment.resourceId !== productionChatMemoryResourceId(req.user.id) + ) { + throw new NotFoundError(`Attachment "${attachmentId}" not found`); + } + await this.requireProductionThread(attachment.threadId, projectId, agentId, req.user.id); + await this.streamAttachment(attachment, res); + } + @Get('/:agentId/chat/:threadId/queue') @ProjectScope('agent:read') async getQueuedMessages( @@ -399,6 +678,17 @@ export class AgentChatController { if (!threadBelongsTo(thread, projectId, agentId, req.user.id)) { throw new NotFoundError(`Thread "${threadId}" not found`); } + if ( + thread.accessScope === 'user' && + !(await this.agentExecutionService.canUseDraftThread( + threadId, + projectId, + agentId, + req.user.id, + { previewChat: true, sessionMode: 'existing' }, + )) + ) + throw new NotFoundError(`Thread "${threadId}" not found`); const jobs = await this.backgroundJobService.listCurrentGroupForThread(agentId, threadId); return { @@ -428,6 +718,17 @@ export class AgentChatController { if (thread && !threadBelongsTo(thread, projectId, agentId, req.user.id)) { throw new NotFoundError(`Thread "${threadId}" not found`); } + if ( + thread?.accessScope === 'user' && + !(await this.agentExecutionService.canUseDraftThread( + threadId, + projectId, + agentId, + req.user.id, + { previewChat: true, sessionMode: 'existing' }, + )) + ) + throw new NotFoundError(`Thread "${threadId}" not found`); const history = await this.agentExecutionOrchestratorService.getConversationHistory({ threadId, projectId, @@ -513,7 +814,14 @@ export class AgentChatController { userId: req.user.id, }); if (!attachment) throw new NotFoundError(`Attachment "${attachmentId}" not found`); + if (attachment.source === N8N_CHAT_PRODUCTION_SOURCE) { + throw new NotFoundError(`Attachment "${attachmentId}" not found`); + } + await this.streamAttachment(attachment, res); + } + private async streamAttachment(attachment: AgentChatAttachment, res: Response) { + const attachmentId = attachment.id; // Open the stream before writing headers: bytes can be gone while the row // remains (out-of-band storage cleanup), and that must surface as a clean // 404 rather than a half-written response. diff --git a/packages/cli/src/modules/agents/agent-config.service.ts b/packages/cli/src/modules/agents/agent-config.service.ts index e02451e30b5..0f9dcc818a5 100644 --- a/packages/cli/src/modules/agents/agent-config.service.ts +++ b/packages/cli/src/modules/agents/agent-config.service.ts @@ -323,7 +323,13 @@ export class AgentConfigService { previousIntegrations, nextIntegrations, ); - return { nextSchema, nextIntegrations, previousSchema, previousIntegrations, changedParts }; + return { + nextSchema, + nextIntegrations, + previousSchema, + previousIntegrations, + changedParts, + }; } private mergeConfigSchema( diff --git a/packages/cli/src/modules/agents/agent-execution-orchestrator.service.ts b/packages/cli/src/modules/agents/agent-execution-orchestrator.service.ts index 7988a8c2f03..79ef421ebca 100644 --- a/packages/cli/src/modules/agents/agent-execution-orchestrator.service.ts +++ b/packages/cli/src/modules/agents/agent-execution-orchestrator.service.ts @@ -29,8 +29,11 @@ import type { AgentSessionMode } from './utils/agent-thread-access'; import { draftChatMemoryResourceId, isTaskRunMemoryResourceId, + productionChatMemoryResourceId, userIdFromDraftChatMemoryResourceId, + userIdFromProductionChatMemoryResourceId, } from './utils/agent-memory-scope'; +import { N8N_CHAT_PRODUCTION_SOURCE } from './utils/agent-thread-access'; import { AgentRunTracingService, modelIdFromSnapshot } from './agent-run-tracing.service'; import { AgentRuntimeCacheService, @@ -133,6 +136,13 @@ export interface ExecuteForChatPublishedConfig extends ChatExecutionInput { // applies regardless. } +export interface ExecuteForN8nChatPublishedConfig + extends ChatExecutionInput, + ChatExecutionCallbacks { + user: User; + abortSignal?: AbortSignal; +} + export interface ResumeForChatConfig extends ChatExecutionCallbacks { messageContext?: IntegrationMessageContext | null; /** Platform conversation metadata scope. Execution memory can belong to a task. */ @@ -207,6 +217,7 @@ export interface ExecuteForWakeConfig extends AgentExecutionInput { export interface StreamChatResponseConfig extends ChatExecutionInput, ChatExecutionCallbacks { onAdmitted?: () => Promise; access: AgentThreadAccess; + productionN8nChat?: boolean; messageContext?: IntegrationMessageContext | null; agentInstance: RuntimeAgent; toolRegistry: ToolRegistry; @@ -342,22 +353,34 @@ export class AgentExecutionOrchestratorService { } private async resolveResumeAccess( - { agentId, projectId, runId, user, usePublishedVersion, previewChat }: ResumeChatConfig, + { agentId, projectId, runId, user, usePublishedVersion, previewChat, source }: ResumeChatConfig, memoryScope: ResumeCheckpoint['memoryScope'], ): Promise { const isPreview = !usePublishedVersion && !isTaskRunMemoryResourceId(memoryScope.resourceId); + const isProductionChat = source === N8N_CHAT_PRODUCTION_SOURCE; let access: AgentThreadAccess = { accessScope: 'project', ownerId: null }; - if (isPreview) { + if (isPreview || isProductionChat) { if ( !user || - memoryScope.resourceId !== draftChatMemoryResourceId(user.id) || - !(await this.agentExecutionService.canUseDraftThread( - memoryScope.threadId, - projectId, - agentId, - user.id, - { previewChat, sessionMode: 'existing' }, - )) + memoryScope.resourceId !== + (isPreview + ? draftChatMemoryResourceId(user.id) + : productionChatMemoryResourceId(user.id)) || + (isPreview + ? !(await this.agentExecutionService.canUseDraftThread( + memoryScope.threadId, + projectId, + agentId, + user.id, + { previewChat, sessionMode: 'existing' }, + )) + : !(await this.agentExecutionService.canUseProductionChatThread( + memoryScope.threadId, + projectId, + agentId, + user.id, + 'existing', + ))) ) { throw new UserError(`Checkpoint ${runId} does not belong to this chat`); } @@ -366,6 +389,7 @@ export class AgentExecutionOrchestratorService { const thread = await this.agentExecutionService.findThreadById(memoryScope.threadId); if ( userIdFromDraftChatMemoryResourceId(memoryScope.resourceId) || + userIdFromProductionChatMemoryResourceId(memoryScope.resourceId) || !thread || thread.projectId !== projectId || thread.agentId !== agentId || @@ -378,17 +402,17 @@ export class AgentExecutionOrchestratorService { } private validateResumeSandbox( - { projectId, runId, user, usePublishedVersion }: ResumeChatConfig, + { projectId, runId, user, usePublishedVersion, source }: ResumeChatConfig, memoryScope: ResumeCheckpoint['memoryScope'], ): AgentSandboxPrincipalHash | undefined { const sandboxScope = decodeAgentSandboxHostMetadata(memoryScope.hostMetadata); const sandboxPrincipalHash = sandboxScope?.principalHash; if ( - this.agentSandboxRuntimeService.isEnabled() && + (this.agentSandboxRuntimeService.isEnabled() || source === N8N_CHAT_PRODUCTION_SOURCE) && (!sandboxScope || sandboxScope.projectId !== projectId || !sandboxPrincipalHash || - (!usePublishedVersion && + ((!usePublishedVersion || source === N8N_CHAT_PRODUCTION_SOURCE) && (!user || sandboxPrincipalHash !== hashAgentSandboxPrincipal({ type: 'n8n-user', userId: user.id })))) @@ -405,6 +429,12 @@ export class AgentExecutionOrchestratorService { */ async *resumeForChat(config: ResumeForChatConfig): AsyncGenerator { const resume = { ...config, usePublishedVersion: config.usePublishedVersion ?? true }; + if ( + resume.source === N8N_CHAT_PRODUCTION_SOURCE && + !(await this.agentRepository.isN8nChatPublished(resume.agentId, resume.projectId)) + ) { + throw new UserError('This agent is not available in n8n Chat'); + } const checkpoint = await this.loadResumeCheckpoint(resume); yield* this.withRuntimeLease( async () => await this.getResumeRuntime(resume, checkpoint), @@ -521,6 +551,92 @@ export class AgentExecutionOrchestratorService { ); } + async *executeForN8nChatPublished( + config: ExecuteForN8nChatPublishedConfig, + ): AsyncGenerator { + const { + agentId, + projectId, + message, + memory, + attachments, + user, + abortSignal, + sessionMode = 'new', + } = config; + if (!(await this.agentRepository.isN8nChatPublished(agentId, projectId))) { + throw new UserError('This agent is not available in n8n Chat'); + } + if ( + memory.resourceId !== productionChatMemoryResourceId(user.id) || + !(await this.agentExecutionService.canUseProductionChatThread( + memory.threadId, + projectId, + agentId, + user.id, + sessionMode, + )) + ) { + throw new UserError('Session not found'); + } + const sandboxPrincipalHash = hashAgentSandboxPrincipal({ type: 'n8n-user', userId: user.id }); + await this.externalHooks.run('agent.preExecute', [agentId]); + yield* this.withRuntimeLease( + async () => + await this.getRuntimeOrRecordFailure( + { + agentId, + projectId, + integrationType: N8N_CHAT_INTEGRATION_TYPE, + usePublishedVersion: true, + sandboxPrincipalHash, + allowBackgroundTasks: false, + attributionUserId: user.id, + }, + { + threadId: memory.threadId, + userMessage: message, + attachments, + source: N8N_CHAT_PRODUCTION_SOURCE, + access: { accessScope: 'user', ownerId: user.id }, + sessionMode, + abortSignal, + }, + ), + async (runtime) => { + const messageContext = this.createN8nChatMessageContext(memory, user.id); + return this.streamChatResponse({ + access: { accessScope: 'user', ownerId: user.id }, + messageContext, + onAdmitted: async () => + await this.integrationMessageContextService.setLatest( + memory.threadId, + memory.resourceId, + messageContext, + ), + agentInstance: runtime.agent, + toolRegistry: runtime.toolRegistry, + mcpServerAttributions: runtime.mcpServerAttributions, + agentId, + projectId, + message, + memory, + attachments, + userId: user.id, + productionN8nChat: true, + source: N8N_CHAT_PRODUCTION_SOURCE, + telemetry: { runType: 'production', configuration: runtime.telemetryConfiguration }, + includeHitlToolDetails: true, + sandboxPrincipalHash, + sessionMode, + abortSignal, + onExecutionStarted: config.onExecutionStarted, + onExecutionRecorded: config.onExecutionRecorded, + }); + }, + ); + } + /** * Execute a published agent for a scheduled task, stamping `source='task'` * and the originating `taskId` on the recorded session for traceability. @@ -632,9 +748,30 @@ export class AgentExecutionOrchestratorService { async executeForWake(config: ExecuteForWakeConfig): Promise { const { agentId, memory, identity, abortSignal } = config; const isDraft = identity.type === 'draft'; + const productionUserId = isDraft + ? undefined + : userIdFromProductionChatMemoryResourceId(memory.resourceId); + if ( + productionUserId && + (isDraft || + identity.integrationType !== N8N_CHAT_INTEGRATION_TYPE || + identity.principalHash !== + hashAgentSandboxPrincipal({ type: 'n8n-user', userId: productionUserId }) || + !(await this.agentRepository.isN8nChatPublished(agentId, config.projectId)) || + !(await this.agentExecutionService.canUseProductionChatThread( + memory.threadId, + config.projectId, + agentId, + productionUserId, + 'existing', + ))) + ) + return; const access: AgentThreadAccess = isDraft ? { accessScope: 'user', ownerId: identity.user.id } - : { accessScope: 'project', ownerId: null }; + : productionUserId + ? { accessScope: 'user', ownerId: productionUserId } + : { accessScope: 'project', ownerId: null }; if (isDraft) { await this.assertDraftChatAccess({ ...config, user: identity.user, sessionMode: 'existing' }); } else { @@ -642,14 +779,22 @@ export class AgentExecutionOrchestratorService { } const integrationType = isDraft ? N8N_CHAT_INTEGRATION_TYPE : identity.integrationType; const messageContext = await this.integrationMessageContextService.getLatest(memory.threadId); - const delivery = isDraft - ? undefined - : await this.getWakeDelivery(agentId, integrationType, messageContext); + const delivery = + isDraft || productionUserId + ? undefined + : await this.getWakeDelivery(agentId, integrationType, messageContext); const stream = this.withRuntimeLease( - async () => await this.getWakeRuntime(config, access, integrationType), + async () => await this.getWakeRuntime(config, access, integrationType, productionUserId), (runtime) => this.streamWakeResponse( - this.streamWakeTurn(config, runtime, access, integrationType, messageContext), + this.streamWakeTurn( + config, + runtime, + access, + integrationType, + messageContext, + productionUserId, + ), abortSignal, delivery, messageContext?.interactingUserId, @@ -731,6 +876,7 @@ export class AgentExecutionOrchestratorService { includeHitlToolDetails: config.includeHitlToolDetails, onExecutionRecorded: config.onExecutionRecorded, previewChat: config.previewChat, + productionN8nChat: config.source === N8N_CHAT_PRODUCTION_SOURCE, onExecutionStarted: config.onExecutionStarted, onSettled: config.isWakeRun ? undefined @@ -817,7 +963,7 @@ export class AgentExecutionOrchestratorService { agentName: selected.schema?.name ?? agent.name, projectId, telemetry: { - userId: params.user?.id, + userId: params.attributionUserId ?? params.user?.id, runType: params.usePublishedVersion ? 'production' : 'test', configuration: buildAgentConfigurationTelemetry(selected), }, @@ -852,6 +998,8 @@ export class AgentExecutionOrchestratorService { usePublishedVersion, integrationType, user: usePublishedVersion ? undefined : user, + attributionUserId: source === N8N_CHAT_PRODUCTION_SOURCE ? user?.id : undefined, + allowBackgroundTasks: source === N8N_CHAT_PRODUCTION_SOURCE ? false : undefined, ...(sandboxPrincipalHash ? { sandboxPrincipalHash } : {}), previewChat, }, @@ -881,8 +1029,10 @@ export class AgentExecutionOrchestratorService { toolRegistry: runtime.toolRegistry, mcpServerAttributions: runtime.mcpServerAttributions, context: { projectId: config.projectId, agentId: config.agentId, threadId }, - includeHitlToolDetails: !config.usePublishedVersion, + includeHitlToolDetails: + !config.usePublishedVersion || config.source === N8N_CHAT_PRODUCTION_SOURCE, previewChat: config.previewChat, + productionN8nChat: config.source === N8N_CHAT_PRODUCTION_SOURCE, automaticPreviewContinuation: config.automaticPreviewContinuation, onExecutionStarted: config.onExecutionStarted, onExecutionRecorded: config.onExecutionRecorded, @@ -1096,7 +1246,7 @@ export class AgentExecutionOrchestratorService { previewChat, sessionMode, } = config; - const messageContext = this.createDraftMessageContext(memory, user.id); + const messageContext = this.createN8nChatMessageContext(memory, user.id); return this.streamChatResponse({ admittedExecution: config.admittedExecution, onAdmitted: async () => @@ -1128,7 +1278,7 @@ export class AgentExecutionOrchestratorService { }); } - private createDraftMessageContext( + private createN8nChatMessageContext( memory: AgentMemoryScope, userId: string, ): IntegrationMessageContext { @@ -1237,6 +1387,7 @@ export class AgentExecutionOrchestratorService { config: ExecuteForWakeConfig, access: AgentThreadAccess, integrationType: string, + productionUserId?: string, ) { const { agentId, projectId, memory, identity, abortSignal } = config; const isDraft = identity.type === 'draft'; @@ -1246,13 +1397,16 @@ export class AgentExecutionOrchestratorService { projectId, integrationType, usePublishedVersion: !isDraft, + ...(productionUserId + ? { attributionUserId: productionUserId, allowBackgroundTasks: false } + : {}), ...(isDraft ? { user: identity.user } : {}), sandboxPrincipalHash: identity.principalHash, }, { threadId: memory.threadId, userMessage: null, - source: integrationType, + source: productionUserId ? N8N_CHAT_PRODUCTION_SOURCE : integrationType, abortSignal, access, sessionMode: 'existing', @@ -1266,6 +1420,7 @@ export class AgentExecutionOrchestratorService { access: AgentThreadAccess, integrationType: string, messageContext: IntegrationMessageContext | null, + productionUserId?: string, ) { const { agentId, message, memory, identity, abortSignal } = config; const isDraft = identity.type === 'draft'; @@ -1276,17 +1431,21 @@ export class AgentExecutionOrchestratorService { toolRegistry: runtime.toolRegistry, mcpServerAttributions: runtime.mcpServerAttributions, agentId, - ...(isDraft ? { userId: identity.user.id } : {}), + ...(isDraft + ? { userId: identity.user.id } + : productionUserId + ? { userId: productionUserId } + : {}), message, memory, projectId: runtime.projectId, - source: integrationType, + source: productionUserId ? N8N_CHAT_PRODUCTION_SOURCE : integrationType, telemetry: { runType: isDraft ? 'test' : 'production', configuration: runtime.telemetryConfiguration, }, abortSignal, - includeHitlToolDetails: isDraft, + includeHitlToolDetails: isDraft || Boolean(productionUserId), sandboxPrincipalHash: identity.principalHash, hideUserMessageFromTranscript: true, isWakeRun: true, diff --git a/packages/cli/src/modules/agents/agent-execution.service.ts b/packages/cli/src/modules/agents/agent-execution.service.ts index e74c25d463d..96a382a40f9 100644 --- a/packages/cli/src/modules/agents/agent-execution.service.ts +++ b/packages/cli/src/modules/agents/agent-execution.service.ts @@ -40,6 +40,7 @@ import { draftChatMemoryResourceId } from './utils/agent-memory-scope'; import { canContinueThreadInPreview, canUseTopLevelDraftThread, + N8N_CHAT_PRODUCTION_SOURCE, threadBelongsTo, type AgentSessionMode, } from './utils/agent-thread-access'; @@ -732,6 +733,26 @@ export class AgentExecutionService { return await this.canUseUnrecordedDraftThread(threadId, agentId, userId); } + async canUseProductionChatThread( + threadId: string, + projectId: string, + agentId: string, + userId: string, + sessionMode: AgentSessionMode, + ): Promise { + const thread = await this.findThreadById(threadId); + if (!thread) return sessionMode === 'new'; + if ( + thread.projectId !== projectId || + thread.agentId !== agentId || + !canUseTopLevelDraftThread(thread, userId) || + thread.taskId !== null + ) + return false; + const sources = await this.agentExecutionRepository.findFirstSourceByThreadIds([threadId]); + return sources.get(threadId) === N8N_CHAT_PRODUCTION_SOURCE; + } + private async canUseUnrecordedDraftThread( threadId: string, agentId: string, diff --git a/packages/cli/src/modules/agents/agent-integrations.controller.ts b/packages/cli/src/modules/agents/agent-integrations.controller.ts index 0827708f040..35b8ee58dbe 100644 --- a/packages/cli/src/modules/agents/agent-integrations.controller.ts +++ b/packages/cli/src/modules/agents/agent-integrations.controller.ts @@ -107,9 +107,25 @@ export class AgentIntegrationsController { const statuses = await this.channelStatusRepository.findByAgentId(agentId); const now = new Date(); - return buildChannelStatusReport(agent.integrations, agent.activeVersionId, statuses, (row) => - this.statusReporter.isLive(row, now), - ); + return { + ...buildChannelStatusReport( + agent.integrations, + agent.activeVersionId, + statuses, + (row) => this.statusReporter.isLive(row, now), + agent.activeVersion?.schema?.integrations ?? [], + ), + n8nChat: { + draftEnabled: + agent.integrations?.some((integration) => integration.type === 'n8n_chat') ?? false, + publishedEnabled: + agent.activeVersionId !== null && + (agent.activeVersion?.schema?.integrations?.some( + (integration) => integration.type === 'n8n_chat', + ) ?? + false), + }, + }; } // Third-party webhook callback: do not add @ProjectScope. Auth happens diff --git a/packages/cli/src/modules/agents/agent-publish.service.ts b/packages/cli/src/modules/agents/agent-publish.service.ts index c27d995721c..257edabfe4a 100644 --- a/packages/cli/src/modules/agents/agent-publish.service.ts +++ b/packages/cli/src/modules/agents/agent-publish.service.ts @@ -1,4 +1,6 @@ import { + N8N_CHAT_INTEGRATION_TYPE, + isCredentialAgentIntegration, isDraftIntegration, type AgentConfigValidationResponse, type AgentJsonConfig, @@ -79,6 +81,13 @@ function requireValidValidation( } } +function draftSchemaFromVersion(schema: AgentJsonConfig | null): AgentJsonConfig | null { + if (!schema) return null; + const draft = deepCopy(schema); + delete draft.integrations; + return draft; +} + export interface PublishAgentResult { agent: Agent; /** @@ -173,8 +182,8 @@ export class AgentPublishService { * about to become live, independent of any frontend check. Validating the * current draft is not enough when a specific historical `versionId` is * being republished — that snapshot's schema/tool/skill bodies must be - * checked instead. Integrations are never versioned, so the agent's - * *current* integrations are always part of the check. + * checked instead. Credential-backed integrations use the current draft. + * The n8n Chat entry is saved in the published schema. */ private async assertPublishable( agent: Agent, @@ -227,6 +236,7 @@ export class AgentPublishService { private async assertChannelsStartable(agent: Agent, projectId: string): Promise { const chatIntegrationService = Container.get(ChatIntegrationService); for (const integration of agent.integrations ?? []) { + if (!isCredentialAgentIntegration(integration)) continue; if (isDraftIntegration(integration)) continue; await chatIntegrationService.assertStartupPreconditions(agent.id, integration, projectId); } @@ -366,7 +376,7 @@ export class AgentPublishService { let tasksChanged = false; await this.agentRepository.manager.transaction(async (trx) => { - agent.schema = activeVersion.schema ? deepCopy(activeVersion.schema) : null; + agent.schema = draftSchemaFromVersion(activeVersion.schema); agent.tools = deepCopy(activeVersion.tools ?? {}); agent.skills = deepCopy(activeVersion.skills ?? {}); agent.versionId = activeVersion.versionId; @@ -417,7 +427,7 @@ export class AgentPublishService { throw new NotFoundError(`Version "${versionId}" not found`); } - agent.schema = target.schema ? deepCopy(target.schema) : null; + agent.schema = draftSchemaFromVersion(target.schema); agent.tools = deepCopy(target.tools ?? {}); agent.skills = deepCopy(target.skills ?? {}); agent.versionId = uuid(); @@ -448,9 +458,8 @@ export class AgentPublishService { } /** - * A revert restores a stored schema wholesale, so it is a modification like - * any other config write. Integrations live outside the schema and survive - * the revert untouched, hence the same list on both sides of the diff. + * A revert restores the schema but leaves draft integrations unchanged. + * The published n8n Chat entry is removed from the restored draft schema. * Sidecar body flags cover tool/skill/task bodies restored outside the schema. */ private async recordRevert( @@ -633,7 +642,7 @@ export class AgentPublishService { private async startPublishedServices(agent: Agent): Promise { const agentId = agent.id; - const credentialIntegrations = agent.integrations ?? []; + const credentialIntegrations = (agent.integrations ?? []).filter(isCredentialAgentIntegration); if (credentialIntegrations.length > 0) { await Container.get(ChatIntegrationService) .syncToConfig(agent, [], credentialIntegrations) @@ -738,7 +747,14 @@ export class AgentPublishService { { versionId, agentId: agent.id, - schema: agent.schema, + schema: agent.schema + ? { + ...agent.schema, + integrations: (agent.integrations ?? []).filter( + (integration) => integration.type === N8N_CHAT_INTEGRATION_TYPE, + ), + } + : null, tools: this.customToolsService.snapshotConfiguredTools(agent.schema, agent.tools ?? {}), skills: this.pickConfiguredSkillBodies(agent.schema, agent.skills ?? {}), publishedBy: user, diff --git a/packages/cli/src/modules/agents/agent-runtime-cache.service.ts b/packages/cli/src/modules/agents/agent-runtime-cache.service.ts index 43184d76c58..a5a7695b0d6 100644 --- a/packages/cli/src/modules/agents/agent-runtime-cache.service.ts +++ b/packages/cli/src/modules/agents/agent-runtime-cache.service.ts @@ -45,6 +45,7 @@ export interface GetRuntimeParams { sandboxPrincipalHash?: AgentSandboxPrincipalHash; /** Disable background-job tools and wake hints for task-triggered runtimes. */ allowBackgroundTasks?: boolean; + attributionUserId?: string; /** * Build for the in-app preview chat, which gets an extra instruction saying * the agent cannot change its own setup. It makes the runtime unusable for @@ -132,6 +133,7 @@ export class AgentRuntimeCacheService { if (params.previewChat) parts.push('preview'); if (params.integrationType) parts.push(params.integrationType); if (params.allowBackgroundTasks === false) parts.push('no-background-tasks'); + if (params.attributionUserId) parts.push(`attribution:${params.attributionUserId}`); // Per-user runtimes have node/workflow tools filtered by that user's // access — keying by user id keeps them from colliding with each other // or with the unscoped (no-user) runtime. @@ -309,6 +311,7 @@ export class AgentRuntimeCacheService { sandboxPrincipalHash, allowBackgroundTasks, previewChat, + attributionUserId, } = params; const agentEntity = await getAgentOrThrow( @@ -343,7 +346,7 @@ export class AgentRuntimeCacheService { undefined, usePublishedVersion ? 'integrated' : 'manual', sandboxPrincipalHash, - { previewChat, allowBackgroundTasks }, + { previewChat, allowBackgroundTasks, attributionUserId }, ); const { agent: agentInstance, diff --git a/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts b/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts index e377b058bd4..1b266bee379 100644 --- a/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts +++ b/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts @@ -14,6 +14,7 @@ import { SUB_AGENT_MAX_CHILDREN_DEFAULT, SUB_AGENT_TASK_DIFFICULTIES, buildProxyHeaders, + isCredentialAgentIntegration, type AgentIntegrationConfig, type AgentJsonConfig, type AgentJsonMcpServerConfig, @@ -141,6 +142,7 @@ export interface ReconstructAgentRuntimeParams extends AgentRuntimeAssets { * integration parents, which keep the project-scoped trust boundary. */ user?: User; + attributionUserId?: string; /** Runtime seams inherited from the delegating parent run (see {@link AgentRuntimeInstrumentation}). */ instrumentation?: AgentRuntimeInstrumentation; sandboxPrincipalHash?: AgentSandboxPrincipalHash; @@ -192,6 +194,7 @@ interface ToolRunIdentity { integrationType?: string; userId?: string; previewChat?: boolean; + publishedN8nChat?: boolean; supportsHitl: boolean; backgroundTasksEnabled: boolean; } @@ -287,6 +290,7 @@ export class AgentRuntimeReconstructionService { supportsHitl, previewChat, allowBackgroundTasks = true, + attributionUserId, }: { /** Pass false when the caller cannot resume a suspended run (workflow executions). */ supportsHitl?: boolean; @@ -294,6 +298,7 @@ export class AgentRuntimeReconstructionService { previewChat?: boolean; /** Disable background jobs for task-triggered runtimes. */ allowBackgroundTasks?: boolean; + attributionUserId?: string; } = {}, ): Promise { let config = agentEntity.schema; @@ -338,6 +343,7 @@ export class AgentRuntimeReconstructionService { credentialIntegrations: agentEntity.integrations ?? [], subAgentDelegation, user, + attributionUserId, instrumentation, sandboxPrincipalHash, unavailableTools, @@ -612,6 +618,7 @@ export class AgentRuntimeReconstructionService { memoryOwnerAgentId, integrationType, user, + attributionUserId, previewChat, supportsHitl, runtimeProfile, @@ -626,8 +633,12 @@ export class AgentRuntimeReconstructionService { usePublishedWorkflowVersion: runType === 'production', agentId: memoryOwnerAgentId, integrationType, - userId: user?.id, + userId: attributionUserId ?? user?.id, previewChat, + publishedN8nChat: + runType === 'production' && + integrationType === N8N_CHAT_INTEGRATION_TYPE && + attributionUserId !== undefined, // Sub-agent checkpoints are rejected on resume and inline agents have no // checkpoint storage, so neither can be woken again. supportsHitl: canResume, @@ -794,6 +805,8 @@ export class AgentRuntimeReconstructionService { agentId, integrationType, userId, + previewChat, + publishedN8nChat, supportsHitl, backgroundTasksEnabled, } = runIdentity; @@ -812,6 +825,8 @@ export class AgentRuntimeReconstructionService { agentId, integrationType, userId, + previewChat, + publishedN8nChat, supportsHitl, backgroundTasksEnabled, }; @@ -922,7 +937,7 @@ export class AgentRuntimeReconstructionService { const descriptors = this.createIntegrationDescriptors( agentId, - credentialIntegrations, + credentialIntegrations.filter(isCredentialAgentIntegration), integrationRegistry, ); diff --git a/packages/cli/src/modules/agents/agent-turn-execution.service.ts b/packages/cli/src/modules/agents/agent-turn-execution.service.ts index 55bced9c990..d6dace399c1 100644 --- a/packages/cli/src/modules/agents/agent-turn-execution.service.ts +++ b/packages/cli/src/modules/agents/agent-turn-execution.service.ts @@ -53,6 +53,7 @@ interface ExecuteTurnConfig { includeHitlToolDetails?: boolean; backgroundJobSignal?: AgentBackgroundJobSignal; previewChat?: boolean; + productionN8nChat?: boolean; automaticPreviewContinuation?: boolean; onExecutionStarted?: (executionId: string, sessionId: string) => void; onExecutionRecorded?: (executionId: string) => void; @@ -134,7 +135,7 @@ export class AgentTurnExecutionService { try { turn = await config.prepare(); const preparedTurn = turn; - if (config.previewChat) { + if (config.previewChat || config.productionN8nChat) { previewControl = this.createPreviewExecutionControl(preparedTurn); preparedTurn.options.abortSignal = previewControl.controller.signal; } @@ -238,7 +239,7 @@ export class AgentTurnExecutionService { ): Promise { const finalize = async () => await this.finalizeTurn(turn, config, recorder, executionId, state); - if (config.previewChat) { + if (config.previewChat || config.productionN8nChat) { await this.chatExecutionService.settle(executionId, finalize, state.suspendedRunId); } else { await finalize(); @@ -433,7 +434,12 @@ export class AgentTurnExecutionService { } if (previewControl) { this.chatExecutionService.register( - { ...config.context, userId: previewControl.userId, executionId }, + { + ...config.context, + userId: previewControl.userId, + executionId, + ...(config.productionN8nChat ? { productionN8nChat: true } : {}), + }, previewControl.controller, ); previewControl.detachRequest(); diff --git a/packages/cli/src/modules/agents/agent-validation.service.ts b/packages/cli/src/modules/agents/agent-validation.service.ts index fc13e77fb5d..ef160bbd691 100644 --- a/packages/cli/src/modules/agents/agent-validation.service.ts +++ b/packages/cli/src/modules/agents/agent-validation.service.ts @@ -210,9 +210,8 @@ export class AgentValidationService { /** * Same as {@link validateAgentConfiguration}, but against a specific * published history snapshot instead of the live draft. Used before - * re-publishing a previously published version. Integrations are not - * versioned, so the agent's *current* integrations are always validated, - * regardless of which historical schema is checked. + * re-publishing a previously published version. Credential-backed + * integrations use the current draft; n8n Chat needs no credential check. */ async validateAgentHistoryConfiguration( agentId: string, @@ -486,6 +485,7 @@ export class AgentValidationService { ) { for (let index = 0; index < integrations.length; index++) { const integration = integrations[index]; + if (integration.type === 'n8n_chat') continue; const path = `integrations.${index}.credentialId`; const capability: AgentConfigValidationIssue['capability'] = { kind: 'channel', diff --git a/packages/cli/src/modules/agents/agent-workflow-tool-resume.service.ts b/packages/cli/src/modules/agents/agent-workflow-tool-resume.service.ts index 1f723a87f26..435a76d1fd1 100644 --- a/packages/cli/src/modules/agents/agent-workflow-tool-resume.service.ts +++ b/packages/cli/src/modules/agents/agent-workflow-tool-resume.service.ts @@ -11,6 +11,10 @@ import { isTerminalExecutionStatus } from 'n8n-workflow'; import { Publisher } from '@/scaling/pubsub/publisher.service'; import { AgentExecutionUpdateBroadcaster } from './agent-execution-update-broadcaster'; +import { AgentExecutionOrchestratorService } from './agent-execution-orchestrator.service'; +import { AgentRepository } from './repositories/agent.repository'; +import { productionChatMemoryResourceId } from './utils/agent-memory-scope'; +import { N8N_CHAT_PRODUCTION_SOURCE } from './utils/agent-thread-access'; import { AgentTestRunService } from './agent-test-run.service'; import { AgentBackgroundJobService, @@ -42,6 +46,8 @@ export class AgentWorkflowToolResumeService { private readonly instanceSettings: InstanceSettings, private readonly publisher: Publisher, private readonly backgroundJobService: AgentBackgroundJobService, + private readonly agentExecutionOrchestratorService: AgentExecutionOrchestratorService, + private readonly agentRepository: AgentRepository, ) { this.logger = this.logger.scoped('agents'); } @@ -140,6 +146,10 @@ export class AgentWorkflowToolResumeService { /** The tool handler re-reads the execution, so this payload only says why it woke. */ async resume(agentRun: RelatedAgentRun, status: string): Promise { const resumeData = { type: 'workflow_finished', value: status }; + if (agentRun.publishedN8nChat === true) { + await this.resumeInProductionChat(agentRun, resumeData); + return; + } if (agentRun.integrationType === N8N_CHAT_INTEGRATION_TYPE) { await this.resumeInPreviewChat(agentRun, resumeData); @@ -191,6 +201,49 @@ export class AgentWorkflowToolResumeService { ); } + private async resumeInProductionChat( + agentRun: RelatedAgentRun, + resumeData: unknown, + ): Promise { + const user = agentRun.userId + ? await this.userRepository.findOneBy({ id: agentRun.userId }) + : null; + if ( + !user || + !(await this.agentRepository.isN8nChatPublished(agentRun.agentId, agentRun.projectId)) + ) + return; + let executionId: string | undefined; + const stream = this.agentExecutionOrchestratorService.resumeForChat({ + agentId: agentRun.agentId, + projectId: agentRun.projectId, + runId: agentRun.runId, + toolCallId: agentRun.toolCallId, + resumeData, + user, + usePublishedVersion: true, + integrationType: N8N_CHAT_INTEGRATION_TYPE, + source: N8N_CHAT_PRODUCTION_SOURCE, + expectedMemory: { + threadId: agentRun.threadId, + resourceId: productionChatMemoryResourceId(user.id), + }, + onExecutionRecorded: (id) => { + executionId = id; + }, + }); + for await (const _chunk of stream) { + // Persist the streamed turn before notifying the user's session. + } + if (executionId) + this.executionUpdateBroadcaster.notify({ + projectId: agentRun.projectId, + agentId: agentRun.agentId, + threadId: agentRun.threadId, + executionId, + }); + } + /** * The preview's SSE stream closed when the run suspended, so there is nothing to * stream into: draining headlessly is what records the turn, and the push then diff --git a/packages/cli/src/modules/agents/agents.service.ts b/packages/cli/src/modules/agents/agents.service.ts index e9741f72d48..273850ce73d 100644 --- a/packages/cli/src/modules/agents/agents.service.ts +++ b/packages/cli/src/modules/agents/agents.service.ts @@ -180,7 +180,10 @@ export class AgentsService { schema: AgentJsonConfig, projectId: string, user: User, - ): Promise<{ schemaConfig: AgentJsonConfig; integrations: AgentIntegrationConfig[] }> { + ): Promise<{ + schemaConfig: AgentJsonConfig; + integrations: AgentIntegrationConfig[]; + }> { const accessibleCredentialIds = new Set( (await createAgentCredentialProvider(this.credentialsService, projectId, user).list()).map( (credential) => credential.id, @@ -195,10 +198,9 @@ export class AgentsService { // The credential-claim check ignores publish state, so a copy holding the // source's channel credentialId would block the original from republishing // or reconnecting (and the reconciler records that 409 on the source's row). - const draftIntegrations = integrations.map((integration) => ({ - ...integration, - credentialId: '', - })); + const draftIntegrations = integrations.map((integration) => + integration.type === 'n8n_chat' ? integration : { ...integration, credentialId: '' }, + ); return { schemaConfig, integrations: draftIntegrations }; } @@ -217,6 +219,10 @@ export class AgentsService { return await this.agentRepository.findByIdAndProjectId(agentId, projectId); } + async isN8nChatPublished(agentId: string, projectId: string): Promise { + return await this.agentRepository.isN8nChatPublished(agentId, projectId); + } + /** * Lightweight capability metadata for the AI Agent node card: the agent's * model plus per-item labels for channels / tools / skills / tasks. Reads the diff --git a/packages/cli/src/modules/agents/builder/agents-builder-tools.service.ts b/packages/cli/src/modules/agents/builder/agents-builder-tools.service.ts index 21461f239b3..c31d9d84a2c 100644 --- a/packages/cli/src/modules/agents/builder/agents-builder-tools.service.ts +++ b/packages/cli/src/modules/agents/builder/agents-builder-tools.service.ts @@ -28,6 +28,7 @@ import { AgentJsonConfigSchema, isDraftAgentConfig, isDraftIntegration, + isCredentialAgentIntegration, sanitizeAgentJsonConfig, tryParseConfigJson, type AgentJsonConfig, @@ -456,7 +457,10 @@ export class AgentsBuilderToolsService { listIntegrationCredentialIds: async () => { const agent = await this.agentsService.findById(agentId, projectId); return (agent?.integrations ?? []) - .filter((integration) => !isDraftIntegration(integration)) + .filter( + (integration) => + isCredentialAgentIntegration(integration) && !isDraftIntegration(integration), + ) .map((integration) => integration.credentialId); }, listChatIntegrationTypes: () => @@ -513,7 +517,10 @@ export class AgentsBuilderToolsService { listIntegrationCredentialIds: async () => { const agent = await this.agentsService.findById(agentId, projectId); return (agent?.integrations ?? []) - .filter((integration) => !isDraftIntegration(integration)) + .filter( + (integration) => + isCredentialAgentIntegration(integration) && !isDraftIntegration(integration), + ) .map((integration) => integration.credentialId); }, track, diff --git a/packages/cli/src/modules/agents/integrations/__tests__/channel-status-report.test.ts b/packages/cli/src/modules/agents/integrations/__tests__/channel-status-report.test.ts index a7aaff35f4a..0f68686de4a 100644 --- a/packages/cli/src/modules/agents/integrations/__tests__/channel-status-report.test.ts +++ b/packages/cli/src/modules/agents/integrations/__tests__/channel-status-report.test.ts @@ -11,6 +11,7 @@ const telegram: AgentIntegrationConfig = { credentialId: 'cred-telegram', settings: { accessMode: 'public', allowedUsers: [] }, }; +const n8nChat: AgentIntegrationConfig = { type: 'n8n_chat', credentialId: '' }; /** Live unless a test says otherwise — expiry is exercised on its own below. */ const isLive = (row: AgentChannelStatus) => @@ -52,6 +53,20 @@ function erroredRow( } describe('buildChannelStatusReport', () => { + it('uses the published n8n Chat entry while the draft changes', () => { + expect(buildChannelStatusReport([], PUBLISHED, [], isLive, [n8nChat])).toEqual({ + status: 'connected', + integrations: [{ type: 'n8n_chat', status: 'connected' }], + }); + expect(buildChannelStatusReport([n8nChat], PUBLISHED, [], isLive)).toEqual({ + status: 'configured', + integrations: [{ type: 'n8n_chat', status: 'configured' }], + }); + expect(buildChannelStatusReport([n8nChat], null, [], isLive, [n8nChat])).toEqual({ + status: 'configured', + integrations: [{ type: 'n8n_chat', status: 'configured' }], + }); + }); it('reports no channels as disconnected', () => { expect(buildChannelStatusReport([], PUBLISHED, [], isLive)).toEqual({ status: 'disconnected', diff --git a/packages/cli/src/modules/agents/integrations/__tests__/chat-integration.service.test.ts b/packages/cli/src/modules/agents/integrations/__tests__/chat-integration.service.test.ts index da760b23dd4..611606992d0 100644 --- a/packages/cli/src/modules/agents/integrations/__tests__/chat-integration.service.test.ts +++ b/packages/cli/src/modules/agents/integrations/__tests__/chat-integration.service.test.ts @@ -201,6 +201,18 @@ describe('ChatIntegrationService.syncToConfig — publish gate', () => { broadcastSpy = vi.spyOn(service, 'broadcastIntegrationChange').mockResolvedValue(); }); + it('does not start an external adapter for n8n Chat', async () => { + const agent = makeAgent({ activeVersionId: 'published-version-1' }); + const n8nChatIntegration = { type: 'n8n_chat', credentialId: '' } as const; + + await service.syncToConfig(agent, [], [n8nChatIntegration]); + await service.syncToConfig(agent, [n8nChatIntegration], []); + + expect(connectSpy).not.toHaveBeenCalled(); + expect(disconnectSpy).not.toHaveBeenCalled(); + expect(broadcastSpy).not.toHaveBeenCalled(); + }); + it('skips connect when the agent is not published', async () => { const agent = makeAgent({ activeVersionId: null }); diff --git a/packages/cli/src/modules/agents/integrations/agent-channel-reconciler.service.ts b/packages/cli/src/modules/agents/integrations/agent-channel-reconciler.service.ts index e091ad15294..d4b88922b83 100644 --- a/packages/cli/src/modules/agents/integrations/agent-channel-reconciler.service.ts +++ b/packages/cli/src/modules/agents/integrations/agent-channel-reconciler.service.ts @@ -1,4 +1,8 @@ -import { isDraftIntegration, type AgentIntegrationConfig } from '@n8n/api-types'; +import { + isCredentialAgentIntegration, + isDraftIntegration, + type AgentIntegrationConfig, +} from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import { AgentsConfig } from '@n8n/config'; import { Time } from '@n8n/constants'; @@ -349,7 +353,10 @@ export class AgentChannelReconciler { const wanted: WantedChannels = new Map(); for (const agent of agents) { const integrations = (agent.integrations ?? []).filter( - (integration) => !isDraftIntegration(integration) && this.runsHere(integration), + (integration) => + isCredentialAgentIntegration(integration) && + !isDraftIntegration(integration) && + this.runsHere(integration), ); for (const integration of integrations) { wanted.set(agentChannelKey(agentChannelRef(agent.id, integration)), { agent, integration }); diff --git a/packages/cli/src/modules/agents/integrations/agent-chat-bridge.ts b/packages/cli/src/modules/agents/integrations/agent-chat-bridge.ts index 4e4685ffeab..71dd03be730 100644 --- a/packages/cli/src/modules/agents/integrations/agent-chat-bridge.ts +++ b/packages/cli/src/modules/agents/integrations/agent-chat-bridge.ts @@ -539,7 +539,10 @@ export class AgentChatBridge { */ private async resolveActiveThreadId(thread: Thread): Promise { const baseId = this.baseThreadId(thread); - const idleTimeoutMinutes = this.integration.settings?.sessionIdleTimeoutMinutes ?? null; + const idleTimeoutMinutes = + 'settings' in this.integration + ? (this.integration.settings?.sessionIdleTimeoutMinutes ?? null) + : null; const id = await this.withSessionLock( baseId, async () => await this.computeGeneration(baseId, false, idleTimeoutMinutes), diff --git a/packages/cli/src/modules/agents/integrations/channel-status-report.ts b/packages/cli/src/modules/agents/integrations/channel-status-report.ts index 6c1518ac9c9..f386851b8e7 100644 --- a/packages/cli/src/modules/agents/integrations/channel-status-report.ts +++ b/packages/cli/src/modules/agents/integrations/channel-status-report.ts @@ -1,4 +1,5 @@ import { + isCredentialAgentIntegration, isDraftIntegration, type AgentChannelRuntimeStatus, type AgentIntegrationConfig, @@ -36,7 +37,8 @@ export function buildChannelStatusReport( activeVersionId: string | null, statuses: AgentChannelStatus[], isLive: IsLiveRow, -): AgentIntegrationStatusResponse { + publishedIntegrations: AgentIntegrationConfig[] = [], +): Omit { const liveByChannel = new Map(); for (const row of statuses) { if (!isLive(row)) continue; @@ -49,6 +51,7 @@ export function buildChannelStatusReport( // keeps channel-setup UIs from rendering a configured state and hiding their // own setup form. const entries: AgentIntegrationStatusEntry[] = (integrations ?? []) + .filter(isCredentialAgentIntegration) .filter((integration) => !isDraftIntegration(integration)) .map((integration) => { const rows = liveByChannel.get(channelKey(integration.type, integration.credentialId)) ?? []; @@ -64,6 +67,13 @@ export function buildChannelStatusReport( ...(failure?.errorMessage ? { errorMessage: failure.errorMessage } : {}), }; }); + const draftChat = integrations?.some((integration) => integration.type === 'n8n_chat') ?? false; + const publishedChat = + activeVersionId !== null && + publishedIntegrations.some((integration) => integration.type === 'n8n_chat'); + if (draftChat || publishedChat) { + entries.push({ type: 'n8n_chat', status: publishedChat ? 'connected' : 'configured' }); + } return { status: rollUp(entries), integrations: entries }; } diff --git a/packages/cli/src/modules/agents/integrations/chat-integration.service.ts b/packages/cli/src/modules/agents/integrations/chat-integration.service.ts index 1a82fc3a0d6..d2c390674ec 100644 --- a/packages/cli/src/modules/agents/integrations/chat-integration.service.ts +++ b/packages/cli/src/modules/agents/integrations/chat-integration.service.ts @@ -1,4 +1,8 @@ -import { AgentIntegrationConfig, type AgentIntegrationSettings } from '@n8n/api-types'; +import { + AgentIntegrationConfig, + isCredentialAgentIntegration, + type AgentIntegrationSettings, +} from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import { GlobalConfig } from '@n8n/config'; import { Time } from '@n8n/constants'; @@ -206,6 +210,7 @@ export class ChatIntegrationService { integration: AgentIntegrationConfig, projectId: string, ): Promise { + if (!isCredentialAgentIntegration(integration)) return; const implementation = this.integrationRegistry.require(integration.type); // Deliberately not `validateConfig`: publishing already validates the whole // configuration, and running it again here would newly reject an agent whose @@ -223,6 +228,7 @@ export class ChatIntegrationService { integration: AgentIntegrationConfig, projectId: string, ): Promise { + if (!isCredentialAgentIntegration(integration)) return; const implementation = this.integrationRegistry.require(integration.type); implementation.validateConfig?.(integration); if (!implementation.onBeforeConnect) return; @@ -258,6 +264,7 @@ export class ChatIntegrationService { projectId: string, options: ConnectOptions = {}, ): Promise { + if (!isCredentialAgentIntegration(integration)) return; const ingress = options.ingressEnabled ?? true; if (!this.shouldRouteToLeader(integration.type, ingress)) { return await this.connectLocal(agentId, integration, projectId, options); @@ -455,6 +462,7 @@ export class ChatIntegrationService { integration: AgentIntegrationConfig, options: DisconnectChannelOptions = {}, ): Promise { + if (!isCredentialAgentIntegration(integration)) return; const { deleteSubscriptions = true } = options; try { @@ -568,16 +576,20 @@ export class ChatIntegrationService { previous: AgentIntegrationConfig[], next: AgentIntegrationConfig[], ): Promise { - const previousKeys = new Set(previous.map(buildIntegrationConnectionId)); - const nextKeys = new Set(next.map(buildIntegrationConnectionId)); + const previousExternal = previous.filter(isCredentialAgentIntegration); + const nextExternal = next.filter(isCredentialAgentIntegration); + const previousKeys = new Set(previousExternal.map(buildIntegrationConnectionId)); + const nextKeys = new Set(nextExternal.map(buildIntegrationConnectionId)); - for (const integration of previous) { + for (const integration of previousExternal) { if (!nextKeys.has(buildIntegrationConnectionId(integration))) { await this.disconnectChannel(agent.id, integration); } } - const additions = next.filter((i) => !previousKeys.has(buildIntegrationConnectionId(i))); + const additions = nextExternal.filter( + (integration) => !previousKeys.has(buildIntegrationConnectionId(integration)), + ); if (additions.length > 0 && !agent.activeVersionId) { this.logger.debug( diff --git a/packages/cli/src/modules/agents/integrations/integration-tool-factory.ts b/packages/cli/src/modules/agents/integrations/integration-tool-factory.ts index a34881f0e60..40b3680e212 100644 --- a/packages/cli/src/modules/agents/integrations/integration-tool-factory.ts +++ b/packages/cli/src/modules/agents/integrations/integration-tool-factory.ts @@ -105,7 +105,9 @@ export function getIntegrationToolConnectionDescriptors( actionToolDefinitions, contextToolGuidance: capabilities?.contextToolGuidance, actionToolGuidance: capabilities?.actionToolGuidance, - ...(integration.approval ? { approval: integration.approval } : {}), + ...('approval' in integration && integration.approval + ? { approval: integration.approval } + : {}), }; }); } diff --git a/packages/cli/src/modules/agents/integrations/integration-tool-types.ts b/packages/cli/src/modules/agents/integrations/integration-tool-types.ts index 4cfa6462d03..c0c838dbd89 100644 --- a/packages/cli/src/modules/agents/integrations/integration-tool-types.ts +++ b/packages/cli/src/modules/agents/integrations/integration-tool-types.ts @@ -82,9 +82,8 @@ export interface IntegrationSubjectPerson { } /** - * Source of a tool connection: a persisted credential integration, or the - * implicit credential-less in-app chat channel (injected per-run, never - * stored on the agent). + * Source of a tool connection: a configured integration, or the implicit + * preview chat channel injected per run. */ export type IntegrationToolConnectionSource = | AgentIntegrationConfig diff --git a/packages/cli/src/modules/agents/integrations/platforms/n8n-chat-integration.ts b/packages/cli/src/modules/agents/integrations/platforms/n8n-chat-integration.ts index 559e0537cd1..c16f6993252 100644 --- a/packages/cli/src/modules/agents/integrations/platforms/n8n-chat-integration.ts +++ b/packages/cli/src/modules/agents/integrations/platforms/n8n-chat-integration.ts @@ -21,10 +21,10 @@ import type { IntegrationActionResult } from '../integration-tools'; /** * The in-app chat channel ("n8n Chat"). * - * Implicit and credential-less: never persisted in an agent's `integrations` - * array and never shown in the integrations catalog. A synthetic tool - * descriptor is injected per-run when the execution comes through the in-app - * chat endpoints (see agent-runtime-reconstruction.service.ts). `respond` + * Credential-less. The integration implementation is always registered. + * A draft channel entry enables production chat after publish. Preview stays + * available without an entry. A synthetic tool descriptor is injected per run + * when execution comes through the chat endpoints. `respond` * does not post anywhere — the card renders from the tool-call input in the * chat UI, and interactive cards suspend through the generic machinery. */ diff --git a/packages/cli/src/modules/agents/json-config/agent-config-composition.ts b/packages/cli/src/modules/agents/json-config/agent-config-composition.ts index f7a683d7550..98fd12e549b 100644 --- a/packages/cli/src/modules/agents/json-config/agent-config-composition.ts +++ b/packages/cli/src/modules/agents/json-config/agent-config-composition.ts @@ -25,5 +25,8 @@ export function decomposeJsonConfig(config: AgentJsonConfig): { integrations: AgentIntegrationConfig[]; } { const { integrations, ...schemaConfig } = config; - return { schemaConfig, integrations: integrations ?? [] }; + return { + schemaConfig, + integrations: integrations ?? [], + }; } diff --git a/packages/cli/src/modules/agents/repositories/agent.repository.ts b/packages/cli/src/modules/agents/repositories/agent.repository.ts index 7feeca53f63..166b42f647b 100644 --- a/packages/cli/src/modules/agents/repositories/agent.repository.ts +++ b/packages/cli/src/modules/agents/repositories/agent.repository.ts @@ -1,3 +1,4 @@ +import { N8N_CHAT_INTEGRATION_TYPE } from '@n8n/api-types'; import type { AgentIntegrationConfig, ListAgentsQueryDto } from '@n8n/api-types'; import { Service } from '@n8n/di'; import { @@ -163,6 +164,15 @@ export class AgentRepository extends Repository { }); } + async isN8nChatPublished(id: string, projectId: string): Promise { + const agent = await this.findByIdAndProjectId(id, projectId); + return ( + agent?.activeVersion?.schema?.integrations?.some( + (integration) => integration.type === N8N_CHAT_INTEGRATION_TYPE, + ) ?? false + ); + } + /** * Finds an agent by ID alone. Agent IDs are globally unique, so this is safe * for callers whose access check does not hinge on a specific project (e.g. diff --git a/packages/cli/src/modules/agents/tools/__tests__/workflow-tool-factory.test.ts b/packages/cli/src/modules/agents/tools/__tests__/workflow-tool-factory.test.ts index ba9382424a6..b51c9869912 100644 --- a/packages/cli/src/modules/agents/tools/__tests__/workflow-tool-factory.test.ts +++ b/packages/cli/src/modules/agents/tools/__tests__/workflow-tool-factory.test.ts @@ -776,6 +776,22 @@ describe('workflow tool → parentAgentRun stamping', () => { expect(executionData?.parentAgentRun).toEqual(expect.objectContaining({ previewChat: true })); }); + it('stamps the production n8n Chat marker so the wake-up uses the published runtime', async () => { + const executionData = await runToolWith( + { + agentId: 'agent-1', + integrationType: N8N_CHAT_INTEGRATION_TYPE, + userId: 'user-1', + publishedN8nChat: true, + }, + agentCtx, + ); + + expect(executionData?.parentAgentRun).toEqual( + expect.objectContaining({ publishedN8nChat: true, userId: 'user-1' }), + ); + }); + it('omits the preview marker for every other draft surface', async () => { const executionData = await runToolWith( { agentId: 'agent-1', integrationType: N8N_CHAT_INTEGRATION_TYPE }, diff --git a/packages/cli/src/modules/agents/tools/workflow-tool-factory.ts b/packages/cli/src/modules/agents/tools/workflow-tool-factory.ts index 322516af7c5..c726e8bf1a6 100644 --- a/packages/cli/src/modules/agents/tools/workflow-tool-factory.ts +++ b/packages/cli/src/modules/agents/tools/workflow-tool-factory.ts @@ -167,6 +167,7 @@ export interface WorkflowToolContext { integrationType?: string; /** The in-app preview chat started this run — see `RelatedAgentRun.previewChat`. */ previewChat?: boolean; + publishedN8nChat?: boolean; userId?: string; /** Whether a suspension can be resumed at all. Defaults to true. */ supportsHitl?: boolean; @@ -643,6 +644,7 @@ function agentRunOf( ...(context.integrationType ? { integrationType: context.integrationType } : {}), ...(context.userId ? { userId: context.userId } : {}), ...(context.previewChat ? { previewChat: true } : {}), + ...(context.publishedN8nChat ? { publishedN8nChat: true } : {}), }; } diff --git a/packages/cli/src/modules/agents/utils/agent-memory-scope.ts b/packages/cli/src/modules/agents/utils/agent-memory-scope.ts index 7e9e9952b8f..61491638586 100644 --- a/packages/cli/src/modules/agents/utils/agent-memory-scope.ts +++ b/packages/cli/src/modules/agents/utils/agent-memory-scope.ts @@ -1,5 +1,6 @@ const INTEGRATION_MEMORY_RESOURCE_PREFIX = 'integration:'; const DRAFT_CHAT_MEMORY_RESOURCE_PREFIX = 'draft-chat:'; +const PRODUCTION_CHAT_MEMORY_RESOURCE_PREFIX = 'n8n-chat-production:'; const THREAD_MEMORY_RESOURCE_PREFIX = 'thread:'; export function threadMemoryResourceId(threadId: string): string { @@ -23,6 +24,16 @@ export function draftChatMemoryResourceId(userId: string): string { return `${DRAFT_CHAT_MEMORY_RESOURCE_PREFIX}${userId}`; } +export function productionChatMemoryResourceId(userId: string): string { + return `${PRODUCTION_CHAT_MEMORY_RESOURCE_PREFIX}${userId}`; +} + +export function userIdFromProductionChatMemoryResourceId(resourceId: string): string | undefined { + if (!resourceId.startsWith(PRODUCTION_CHAT_MEMORY_RESOURCE_PREFIX)) return undefined; + const userId = resourceId.slice(PRODUCTION_CHAT_MEMORY_RESOURCE_PREFIX.length); + return userId.length > 0 ? userId : undefined; +} + export function userIdFromDraftChatMemoryResourceId(resourceId: string): string | undefined { if (!resourceId.startsWith(DRAFT_CHAT_MEMORY_RESOURCE_PREFIX)) return undefined; const userId = resourceId.slice(DRAFT_CHAT_MEMORY_RESOURCE_PREFIX.length); diff --git a/packages/cli/src/modules/agents/utils/agent-thread-access.ts b/packages/cli/src/modules/agents/utils/agent-thread-access.ts index 48d64f1015c..187f8f3293f 100644 --- a/packages/cli/src/modules/agents/utils/agent-thread-access.ts +++ b/packages/cli/src/modules/agents/utils/agent-thread-access.ts @@ -3,6 +3,7 @@ import type { AgentExecutionThread } from '../entities/agent-execution-thread.en export type AgentSessionMode = 'new' | 'existing'; export const PREVIEW_THREAD_SOURCES = ['', 'chat', 'n8n_chat'] as const; +export const N8N_CHAT_PRODUCTION_SOURCE = 'n8n_chat_production'; export function threadBelongsTo( thread: AgentExecutionThread, diff --git a/packages/cli/src/scaling/pubsub/pubsub.event-map.ts b/packages/cli/src/scaling/pubsub/pubsub.event-map.ts index e77eb7645a5..89434f9f950 100644 --- a/packages/cli/src/scaling/pubsub/pubsub.event-map.ts +++ b/packages/cli/src/scaling/pubsub/pubsub.event-map.ts @@ -145,6 +145,7 @@ export type PubSubCommandMap = { threadId: string; executionId: string; userId: string; + productionN8nChat?: boolean; }; 'relay-agent-message-queue-update': { diff --git a/packages/cli/test/integration/agents/cassettes/n8n-chat-production/allows-only-the-session-owner-to-cancel-a-suspended-run.json b/packages/cli/test/integration/agents/cassettes/n8n-chat-production/allows-only-the-session-owner-to-cancel-a-suspended-run.json new file mode 100644 index 00000000000..59650226f2c --- /dev/null +++ b/packages/cli/test/integration/agents/cassettes/n8n-chat-production/allows-only-the-session-owner-to-cancel-a-suspended-run.json @@ -0,0 +1,12 @@ +[ + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 10 child sub-agent runs can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n- write_todos helps you plan and track complex objectives before and during execution. It updates the current task list only; it does not complete tasks, run sub-agents, or answer the user.\nWHEN TO USE write_todos:\n- The user request has 3+ meaningful steps or multiple deliverables.\n- The request decomposes into 2+ independent workstreams.\n- Some workstreams are good candidates for delegate_subagent.\n- You need to track progress, revise the plan, or avoid losing context.\nWHEN NOT TO USE write_todos:\n- The request is trivial, conversational, or informational.\n- The task can be completed directly in one or two simple steps.\n- You would only create a todo list to restate the user's request.\nHOW TO USE write_todos:\n- Write concrete, self-contained tasks, not vague phases.\n- Assign difficulty to every task: low for simple mechanical or localized work, medium for normal implementation or review requiring judgment, high for complex, broad, ambiguous, or high-risk work.\n- Mark the first active task, or independent active tasks, as in_progress immediately.\n- For sub-agent-worthy work, create one todo per bounded workstream, then call delegate_subagent separately for that task; pass the same difficulty on the delegate call.\n- Do not delegate the entire user request as one task.\n- Update task status as soon as work completes; do not batch completions at the end.\n- Revise the list when new information changes the plan.\n- Do not call write_todos multiple times in parallel; send one full list update at a time.\n- After all work is done, send the final answer as normal assistant text after the last write_todos call.\n\n\nWhen asked to record a marker, call approve_marker with that marker."},{"role":"user","content":[{"type":"input_text","text":"Record marker B17 using approve_marker."}]}],"tools":[{"type":"function","name":"approve_marker","description":"Approve a marker before recording it","parameters":{"type":"object","properties":{"marker":{"type":"string"}},"required":["marker"]}},{"type":"function","name":"get_environment","description":"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \"today\", deadlines, or scheduling. Tool always requires an empty object as input.","parameters":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_context","description":"Read context from the n8n_chat integration connection.\n\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\n\nQuery inputs:\n\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\n\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\n\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\n\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\n\nUse this tool for read-only lookup before choosing an action target.","parameters":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}},"queries":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}}},"required":["query"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_action","description":"Take actions in the n8n_chat integration connection.\n\nAvailable actions: respond.\n\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\n\nAction inputs:\n\n- respond: input.message is required. Responds in the latest message context for this integration connection.\n\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\n\nrespond uses the latest message context for this integration connection.\n\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\n\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\n\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\n\nFor fields components, use { type: \"fields\", fields: [{ label: \"Account\", value: \"Acme\" }] }. The key items is also accepted as a fields alias.\n\nFor button components, use { type: \"button\", label: \"Approve\", value: \"approve\" }. If label is omitted, text is used as the button label.\n\nFor radio-style choices, use { type: \"radio_select\", label: \"Next step\", options: [{ label: \"Approve\", value: \"approve\" }] }.\n\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\n\nGeneric card examples:\n\nRadio choice card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Acme Executive Briefing — Next Steps\",\n \"card\": {\n \"title\": \"Acme Corporation — Executive Briefing Next Steps\",\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\n\\nWhat should happen next?\"\n },\n {\n \"type\": \"divider\"\n },\n {\n \"type\": \"radio_select\",\n \"id\": \"next_step_selection\",\n \"label\": \"Next step\",\n \"options\": [\n {\n \"label\": \"Schedule exec briefing prep call with Paul G\",\n \"value\": \"schedule_prep_call\"\n },\n {\n \"label\": \"Draft briefing agenda doc for Mike D\",\n \"value\": \"draft_briefing_doc\"\n }\n ]\n }\n ]\n }\n }\n }\n}\n```\n\nButton card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Approve or revise the briefing draft\",\n \"card\": {\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"Review the briefing draft.\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Approve\",\n \"value\": \"approve\",\n \"style\": \"primary\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Revise\",\n \"value\": \"revise\",\n \"style\": \"default\"\n }\n ]\n }\n }\n }\n}\n```\n\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \"plain_text\", text: \"...\" } or { type: \"mrkdwn\", text: \"...\" }.\n\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\n\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.","parameters":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}},"actions":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}}},"required":["action"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"delegate_subagent","description":"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.","parameters":{"type":"object","properties":{"subAgentId":{"type":"string","minLength":1,"description":"Required. Use \"inline\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task."},"taskName":{"type":"string","minLength":1,"description":"Short human-readable name for this delegated task, e.g. \"research_api\"."},"goal":{"type":"string","minLength":1,"description":"The concrete goal the sub-agent should accomplish."},"context":{"type":"string","description":"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out."},"expectedOutput":{"type":"string","description":"The expected shape or contents of the answer."},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning."}},"required":["subAgentId","taskName","goal"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"write_todos","description":"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.","parameters":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Stable identifier for this task within the current plan."},"content":{"type":"string","minLength":1,"description":"Concrete, self-contained task description."},"status":{"type":"string","enum":["pending","in_progress","completed","blocked","cancelled"]},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Task difficulty: \"low\", \"medium\", or \"high\". Use the same value when delegating this task with delegate_subagent."},"delegateHint":{"type":"object","properties":{"subAgentId":{"type":"string","description":"Optional sub-agent id when this task is a delegate_subagent candidate. Use \"inline\" for one-off inline sub-agents."},"expectedOutput":{"type":"string","description":"Optional expected output shape when delegating this task."}},"additionalProperties":false}},"required":["id","content","status","difficulty"],"additionalProperties":false},"description":"Full task list for the current run. Replaces any previous list."}},"required":["todos"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"tool_choice":"auto","stream":true}, + "status": 200, + "response": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0a26a3fd93c0bead006ab433ef57c487d29b83f02b3985d03a\",\"object\":\"response\",\"created_at\":1790194671,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0a26a3fd93c0bead006ab433ef57c487d29b83f02b3985d03a\",\"object\":\"response\",\"created_at\":1790194671,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_Tx01tWwCOMBZW8w8JTYsCYFQ\",\"name\":\"approve_marker\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"obfuscation\":\"edo8es2ddc4jp8\",\"output_index\":0,\"sequence_number\":3}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"marker\",\"item_id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"obfuscation\":\"XVG9iPHlnV\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"obfuscation\":\"WOizNWSI1lj9w\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"B\",\"item_id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"obfuscation\":\"o5tuZBiUHeSGpSC\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"17\",\"item_id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"obfuscation\":\"rOBIBCaMT1XUVO\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"obfuscation\":\"dgjRiSIVvYzeer\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"marker\\\":\\\"B17\\\"}\",\"item_id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"output_index\":0,\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"marker\\\":\\\"B17\\\"}\",\"call_id\":\"call_Tx01tWwCOMBZW8w8JTYsCYFQ\",\"name\":\"approve_marker\"},\"output_index\":0,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0a26a3fd93c0bead006ab433ef57c487d29b83f02b3985d03a\",\"object\":\"response\",\"created_at\":1790194671,\"status\":\"completed\",\"access_programs\":null,\"background\":false,\"completed_at\":1790194672,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"fc_0a26a3fd93c0bead006ab433effa3c87d29a57424f1021a889\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"marker\\\":\\\"B17\\\"}\",\"call_id\":\"call_Tx01tWwCOMBZW8w8JTYsCYFQ\",\"name\":\"approve_marker\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":2604,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":17,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":2621},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3c370d8e223b-MAN","connection":"keep-alive","content-type":"text/event-stream; charset=utf-8","date":"Wed, 23 Sep 2026 20:17:51 GMT","openai-processing-ms":"535","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","transfer-encoding":"chunked","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149997180","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"1ms","x-request-id":"req_18ac0db7461c47acbba8b504843939d7"}, + "responseIsBinary": false + } +] diff --git a/packages/cli/test/integration/agents/cassettes/n8n-chat-production/sends-an-image-to-the-model-and-reloads-the-stored-attachment.json b/packages/cli/test/integration/agents/cassettes/n8n-chat-production/sends-an-image-to-the-model-and-reloads-the-stored-attachment.json new file mode 100644 index 00000000000..7c2ac5baa27 --- /dev/null +++ b/packages/cli/test/integration/agents/cassettes/n8n-chat-production/sends-an-image-to-the-model-and-reloads-the-stored-attachment.json @@ -0,0 +1,22 @@ +[ + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 10 child sub-agent runs can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n- write_todos helps you plan and track complex objectives before and during execution. It updates the current task list only; it does not complete tasks, run sub-agents, or answer the user.\nWHEN TO USE write_todos:\n- The user request has 3+ meaningful steps or multiple deliverables.\n- The request decomposes into 2+ independent workstreams.\n- Some workstreams are good candidates for delegate_subagent.\n- You need to track progress, revise the plan, or avoid losing context.\nWHEN NOT TO USE write_todos:\n- The request is trivial, conversational, or informational.\n- The task can be completed directly in one or two simple steps.\n- You would only create a todo list to restate the user's request.\nHOW TO USE write_todos:\n- Write concrete, self-contained tasks, not vague phases.\n- Assign difficulty to every task: low for simple mechanical or localized work, medium for normal implementation or review requiring judgment, high for complex, broad, ambiguous, or high-risk work.\n- Mark the first active task, or independent active tasks, as in_progress immediately.\n- For sub-agent-worthy work, create one todo per bounded workstream, then call delegate_subagent separately for that task; pass the same difficulty on the delegate call.\n- Do not delegate the entire user request as one task.\n- Update task status as soon as work completes; do not batch completions at the end.\n- Revise the list when new information changes the plan.\n- Do not call write_todos multiple times in parallel; send one full list update at a time.\n- After all work is done, send the final answer as normal assistant text after the last write_todos call.\n\n\nDescribe the color of an attached image in one word."},{"role":"user","content":[{"type":"input_text","text":"What color is this image?"},{"type":"input_image","image_url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"}]}],"tools":[{"type":"function","name":"get_environment","description":"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \"today\", deadlines, or scheduling. Tool always requires an empty object as input.","parameters":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_context","description":"Read context from the n8n_chat integration connection.\n\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\n\nQuery inputs:\n\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\n\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\n\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\n\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\n\nUse this tool for read-only lookup before choosing an action target.","parameters":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}},"queries":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}}},"required":["query"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_action","description":"Take actions in the n8n_chat integration connection.\n\nAvailable actions: respond.\n\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\n\nAction inputs:\n\n- respond: input.message is required. Responds in the latest message context for this integration connection.\n\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\n\nrespond uses the latest message context for this integration connection.\n\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\n\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\n\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\n\nFor fields components, use { type: \"fields\", fields: [{ label: \"Account\", value: \"Acme\" }] }. The key items is also accepted as a fields alias.\n\nFor button components, use { type: \"button\", label: \"Approve\", value: \"approve\" }. If label is omitted, text is used as the button label.\n\nFor radio-style choices, use { type: \"radio_select\", label: \"Next step\", options: [{ label: \"Approve\", value: \"approve\" }] }.\n\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\n\nGeneric card examples:\n\nRadio choice card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Acme Executive Briefing — Next Steps\",\n \"card\": {\n \"title\": \"Acme Corporation — Executive Briefing Next Steps\",\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\n\\nWhat should happen next?\"\n },\n {\n \"type\": \"divider\"\n },\n {\n \"type\": \"radio_select\",\n \"id\": \"next_step_selection\",\n \"label\": \"Next step\",\n \"options\": [\n {\n \"label\": \"Schedule exec briefing prep call with Paul G\",\n \"value\": \"schedule_prep_call\"\n },\n {\n \"label\": \"Draft briefing agenda doc for Mike D\",\n \"value\": \"draft_briefing_doc\"\n }\n ]\n }\n ]\n }\n }\n }\n}\n```\n\nButton card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Approve or revise the briefing draft\",\n \"card\": {\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"Review the briefing draft.\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Approve\",\n \"value\": \"approve\",\n \"style\": \"primary\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Revise\",\n \"value\": \"revise\",\n \"style\": \"default\"\n }\n ]\n }\n }\n }\n}\n```\n\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \"plain_text\", text: \"...\" } or { type: \"mrkdwn\", text: \"...\" }.\n\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\n\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.","parameters":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}},"actions":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}}},"required":["action"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"delegate_subagent","description":"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.","parameters":{"type":"object","properties":{"subAgentId":{"type":"string","minLength":1,"description":"Required. Use \"inline\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task."},"taskName":{"type":"string","minLength":1,"description":"Short human-readable name for this delegated task, e.g. \"research_api\"."},"goal":{"type":"string","minLength":1,"description":"The concrete goal the sub-agent should accomplish."},"context":{"type":"string","description":"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out."},"expectedOutput":{"type":"string","description":"The expected shape or contents of the answer."},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning."}},"required":["subAgentId","taskName","goal"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"write_todos","description":"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.","parameters":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Stable identifier for this task within the current plan."},"content":{"type":"string","minLength":1,"description":"Concrete, self-contained task description."},"status":{"type":"string","enum":["pending","in_progress","completed","blocked","cancelled"]},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Task difficulty: \"low\", \"medium\", or \"high\". Use the same value when delegating this task with delegate_subagent."},"delegateHint":{"type":"object","properties":{"subAgentId":{"type":"string","description":"Optional sub-agent id when this task is a delegate_subagent candidate. Use \"inline\" for one-off inline sub-agents."},"expectedOutput":{"type":"string","description":"Optional expected output shape when delegating this task."}},"additionalProperties":false}},"required":["id","content","status","difficulty"],"additionalProperties":false},"description":"Full task list for the current run. Replaces any previous list."}},"required":["todos"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"tool_choice":"auto","stream":true}, + "status": 200, + "response": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_01fb8a948ff3bb94006ab433ae2df487d2ab13e08eb208a913\",\"object\":\"response\",\"created_at\":1790194606,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_01fb8a948ff3bb94006ab433ae2df487d2ab13e08eb208a913\",\"object\":\"response\",\"created_at\":1790194606,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_01fb8a948ff3bb94006ab433aee08c87d2b76bc5430bf85a80\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_01fb8a948ff3bb94006ab433aee08c87d2b76bc5430bf85a80\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Red\",\"item_id\":\"msg_01fb8a948ff3bb94006ab433aee08c87d2b76bc5430bf85a80\",\"logprobs\":[],\"obfuscation\":\"DD5aFMVoe59LZ\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_01fb8a948ff3bb94006ab433aee08c87d2b76bc5430bf85a80\",\"logprobs\":[],\"obfuscation\":\"9YBk7ReLA1rpJow\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_01fb8a948ff3bb94006ab433aee08c87d2b76bc5430bf85a80\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":6,\"text\":\"Red.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_01fb8a948ff3bb94006ab433aee08c87d2b76bc5430bf85a80\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Red.\"},\"sequence_number\":7}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_01fb8a948ff3bb94006ab433aee08c87d2b76bc5430bf85a80\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Red.\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":8}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_01fb8a948ff3bb94006ab433ae2df487d2ab13e08eb208a913\",\"object\":\"response\",\"created_at\":1790194606,\"status\":\"completed\",\"access_programs\":null,\"background\":false,\"completed_at\":1790194606,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_01fb8a948ff3bb94006ab433aee08c87d2b76bc5430bf85a80\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Red.\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":11077,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":4,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":11081},\"user\":null,\"metadata\":{}},\"sequence_number\":9}\n\n", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3a9fbd9b58b3-MAN","connection":"keep-alive","content-type":"text/event-stream; charset=utf-8","date":"Wed, 23 Sep 2026 20:16:46 GMT","openai-processing-ms":"727","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","transfer-encoding":"chunked","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149999235","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"0s","x-request-id":"req_19f22d7bfc784a6c8bc36df92a19cf98"}, + "responseIsBinary": false + }, + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"Generate a short title and a single emoji for a conversation based on the user message.\nRespond with ONLY a JSON object: {\"title\": \"...\", \"emoji\": \"...\"}\nRules:\n- Title: 2 to 6 words, max 50 characters, sentence case\n- Title must not contain emoji, quotes, colons, or markdown formatting\n- Emoji: exactly one emoji that represents the topic\n- Respond with the JSON object only, no other text"},{"role":"user","content":[{"type":"input_text","text":"\nHere's the conversation:\n\nWhat color is this image?\n"}]}]}, + "status": 200, + "response": "{\n \"id\": \"resp_09781f301d6c7b2e006ab433af735c87d2a012bace042c5b14\",\n \"object\": \"response\",\n \"created_at\": 1790194607,\n \"status\": \"completed\",\n \"access_programs\": null,\n \"background\": false,\n \"billing\": {\n \"payer\": \"developer\"\n },\n \"completed_at\": 1790194607,\n \"error\": null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n \"id\": \"msg_09781f301d6c7b2e006ab433afd9d087d29ee2a11b27599dac\",\n \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": [],\n \"text\": \"{\\\"title\\\":\\\"Identifying colors in images\\\",\\\"emoji\\\":\\\"\\ud83c\\udfa8\\\"}\"\n }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": \"in_memory\",\n \"reasoning\": {\n \"context\": null,\n \"effort\": null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tool_usage\": {\n \"image_gen\": {\n \"input_tokens\": 0,\n \"input_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"output_tokens\": 0,\n \"output_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"total_tokens\": 0\n },\n \"web_search\": {\n \"num_requests\": 0\n }\n },\n \"tools\": [],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": 120,\n \"input_tokens_details\": {\n \"cache_write_tokens\": 0,\n \"cached_tokens\": 0\n },\n \"output_tokens\": 15,\n \"output_tokens_details\": {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 135\n },\n \"user\": null,\n \"metadata\": {}\n}", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3aa7d9d758b3-MAN","connection":"keep-alive","content-type":"application/json","date":"Wed, 23 Sep 2026 20:16:48 GMT","openai-processing-ms":"681","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149999860","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"0s","x-request-id":"req_1c62575c0ca84de583ae51c00e6c7ed3"}, + "responseIsBinary": false + } +] diff --git a/packages/cli/test/integration/agents/cassettes/n8n-chat-production/streams-the-published-instructions-and-records-a-private-production-session.json b/packages/cli/test/integration/agents/cassettes/n8n-chat-production/streams-the-published-instructions-and-records-a-private-production-session.json new file mode 100644 index 00000000000..36ef0e25588 --- /dev/null +++ b/packages/cli/test/integration/agents/cassettes/n8n-chat-production/streams-the-published-instructions-and-records-a-private-production-session.json @@ -0,0 +1,42 @@ +[ + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 10 child sub-agent runs can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n- write_todos helps you plan and track complex objectives before and during execution. It updates the current task list only; it does not complete tasks, run sub-agents, or answer the user.\nWHEN TO USE write_todos:\n- The user request has 3+ meaningful steps or multiple deliverables.\n- The request decomposes into 2+ independent workstreams.\n- Some workstreams are good candidates for delegate_subagent.\n- You need to track progress, revise the plan, or avoid losing context.\nWHEN NOT TO USE write_todos:\n- The request is trivial, conversational, or informational.\n- The task can be completed directly in one or two simple steps.\n- You would only create a todo list to restate the user's request.\nHOW TO USE write_todos:\n- Write concrete, self-contained tasks, not vague phases.\n- Assign difficulty to every task: low for simple mechanical or localized work, medium for normal implementation or review requiring judgment, high for complex, broad, ambiguous, or high-risk work.\n- Mark the first active task, or independent active tasks, as in_progress immediately.\n- For sub-agent-worthy work, create one todo per bounded workstream, then call delegate_subagent separately for that task; pass the same difficulty on the delegate call.\n- Do not delegate the entire user request as one task.\n- Update task status as soon as work completes; do not batch completions at the end.\n- Revise the list when new information changes the plan.\n- Do not call write_todos multiple times in parallel; send one full list update at a time.\n- After all work is done, send the final answer as normal assistant text after the last write_todos call.\n\n\nReply with exactly PUBLISHED_MARKER to greeting requests."},{"role":"user","content":[{"type":"input_text","text":"Give me the greeting marker."}]}],"tools":[{"type":"function","name":"get_environment","description":"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \"today\", deadlines, or scheduling. Tool always requires an empty object as input.","parameters":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_context","description":"Read context from the n8n_chat integration connection.\n\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\n\nQuery inputs:\n\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\n\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\n\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\n\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\n\nUse this tool for read-only lookup before choosing an action target.","parameters":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}},"queries":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}}},"required":["query"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_action","description":"Take actions in the n8n_chat integration connection.\n\nAvailable actions: respond.\n\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\n\nAction inputs:\n\n- respond: input.message is required. Responds in the latest message context for this integration connection.\n\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\n\nrespond uses the latest message context for this integration connection.\n\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\n\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\n\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\n\nFor fields components, use { type: \"fields\", fields: [{ label: \"Account\", value: \"Acme\" }] }. The key items is also accepted as a fields alias.\n\nFor button components, use { type: \"button\", label: \"Approve\", value: \"approve\" }. If label is omitted, text is used as the button label.\n\nFor radio-style choices, use { type: \"radio_select\", label: \"Next step\", options: [{ label: \"Approve\", value: \"approve\" }] }.\n\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\n\nGeneric card examples:\n\nRadio choice card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Acme Executive Briefing — Next Steps\",\n \"card\": {\n \"title\": \"Acme Corporation — Executive Briefing Next Steps\",\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\n\\nWhat should happen next?\"\n },\n {\n \"type\": \"divider\"\n },\n {\n \"type\": \"radio_select\",\n \"id\": \"next_step_selection\",\n \"label\": \"Next step\",\n \"options\": [\n {\n \"label\": \"Schedule exec briefing prep call with Paul G\",\n \"value\": \"schedule_prep_call\"\n },\n {\n \"label\": \"Draft briefing agenda doc for Mike D\",\n \"value\": \"draft_briefing_doc\"\n }\n ]\n }\n ]\n }\n }\n }\n}\n```\n\nButton card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Approve or revise the briefing draft\",\n \"card\": {\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"Review the briefing draft.\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Approve\",\n \"value\": \"approve\",\n \"style\": \"primary\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Revise\",\n \"value\": \"revise\",\n \"style\": \"default\"\n }\n ]\n }\n }\n }\n}\n```\n\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \"plain_text\", text: \"...\" } or { type: \"mrkdwn\", text: \"...\" }.\n\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\n\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.","parameters":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}},"actions":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}}},"required":["action"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"delegate_subagent","description":"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.","parameters":{"type":"object","properties":{"subAgentId":{"type":"string","minLength":1,"description":"Required. Use \"inline\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task."},"taskName":{"type":"string","minLength":1,"description":"Short human-readable name for this delegated task, e.g. \"research_api\"."},"goal":{"type":"string","minLength":1,"description":"The concrete goal the sub-agent should accomplish."},"context":{"type":"string","description":"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out."},"expectedOutput":{"type":"string","description":"The expected shape or contents of the answer."},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning."}},"required":["subAgentId","taskName","goal"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"write_todos","description":"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.","parameters":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Stable identifier for this task within the current plan."},"content":{"type":"string","minLength":1,"description":"Concrete, self-contained task description."},"status":{"type":"string","enum":["pending","in_progress","completed","blocked","cancelled"]},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Task difficulty: \"low\", \"medium\", or \"high\". Use the same value when delegating this task with delegate_subagent."},"delegateHint":{"type":"object","properties":{"subAgentId":{"type":"string","description":"Optional sub-agent id when this task is a delegate_subagent candidate. Use \"inline\" for one-off inline sub-agents."},"expectedOutput":{"type":"string","description":"Optional expected output shape when delegating this task."}},"additionalProperties":false}},"required":["id","content","status","difficulty"],"additionalProperties":false},"description":"Full task list for the current run. Replaces any previous list."}},"required":["todos"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"tool_choice":"auto","stream":true}, + "status": 200, + "response": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_05d7c5655947a4ef006ab433a454b487d2b525830aeb4b3a69\",\"object\":\"response\",\"created_at\":1790194596,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_05d7c5655947a4ef006ab433a454b487d2b525830aeb4b3a69\",\"object\":\"response\",\"created_at\":1790194596,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"P\",\"item_id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"logprobs\":[],\"obfuscation\":\"1jQi7neYoQKHYeZ\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"UBLISHED\",\"item_id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"logprobs\":[],\"obfuscation\":\"YBdBMnrW\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"_MARK\",\"item_id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"logprobs\":[],\"obfuscation\":\"dt8Awj96mJg\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ER\",\"item_id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"logprobs\":[],\"obfuscation\":\"deg4zxgOZ5OlS5\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":8,\"text\":\"PUBLISHED_MARKER\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"PUBLISHED_MARKER\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"PUBLISHED_MARKER\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_05d7c5655947a4ef006ab433a454b487d2b525830aeb4b3a69\",\"object\":\"response\",\"created_at\":1790194596,\"status\":\"completed\",\"access_programs\":null,\"background\":false,\"completed_at\":1790194597,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_05d7c5655947a4ef006ab433a5acd087d2969ebc2719d3a73a\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"PUBLISHED_MARKER\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":2577,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":6,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":2583},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3a61eae458b3-MAN","connection":"keep-alive","content-type":"text/event-stream; charset=utf-8","date":"Wed, 23 Sep 2026 20:16:37 GMT","openai-processing-ms":"1365","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","transfer-encoding":"chunked","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149997205","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"1ms","x-request-id":"req_88a56315f3bb45749fcff961afedd604"}, + "responseIsBinary": false + }, + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"Generate a short title and a single emoji for a conversation based on the user message.\nRespond with ONLY a JSON object: {\"title\": \"...\", \"emoji\": \"...\"}\nRules:\n- Title: 2 to 6 words, max 50 characters, sentence case\n- Title must not contain emoji, quotes, colons, or markdown formatting\n- Emoji: exactly one emoji that represents the topic\n- Respond with the JSON object only, no other text"},{"role":"user","content":[{"type":"input_text","text":"\nHere's the conversation:\n\nGive me the greeting marker.\n"}]}]}, + "status": 200, + "response": "{\n \"id\": \"resp_09f7cd384e3dba65006ab433a67ad487d2813823dfe09b0361\",\n \"object\": \"response\",\n \"created_at\": 1790194598,\n \"status\": \"completed\",\n \"access_programs\": null,\n \"background\": false,\n \"billing\": {\n \"payer\": \"developer\"\n },\n \"completed_at\": 1790194599,\n \"error\": null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n \"id\": \"msg_09f7cd384e3dba65006ab433a6e4c887d2a888d9edc3dda99b\",\n \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": [],\n \"text\": \"{\\\"title\\\":\\\"Request for a greeting marker\\\",\\\"emoji\\\":\\\"\\ud83d\\udc4b\\\"}\"\n }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": \"in_memory\",\n \"reasoning\": {\n \"context\": null,\n \"effort\": null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tool_usage\": {\n \"image_gen\": {\n \"input_tokens\": 0,\n \"input_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"output_tokens\": 0,\n \"output_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"total_tokens\": 0\n },\n \"web_search\": {\n \"num_requests\": 0\n }\n },\n \"tools\": [],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": 120,\n \"input_tokens_details\": {\n \"cache_write_tokens\": 0,\n \"cached_tokens\": 0\n },\n \"output_tokens\": 15,\n \"output_tokens_details\": {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 135\n },\n \"user\": null,\n \"metadata\": {}\n}", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3a6fefa558b3-MAN","connection":"keep-alive","content-type":"application/json","date":"Wed, 23 Sep 2026 20:16:39 GMT","openai-processing-ms":"718","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149999862","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"0s","x-request-id":"req_6179ad09461045f38ee4c43faa21bfe3"}, + "responseIsBinary": false + }, + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 10 child sub-agent runs can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n- write_todos helps you plan and track complex objectives before and during execution. It updates the current task list only; it does not complete tasks, run sub-agents, or answer the user.\nWHEN TO USE write_todos:\n- The user request has 3+ meaningful steps or multiple deliverables.\n- The request decomposes into 2+ independent workstreams.\n- Some workstreams are good candidates for delegate_subagent.\n- You need to track progress, revise the plan, or avoid losing context.\nWHEN NOT TO USE write_todos:\n- The request is trivial, conversational, or informational.\n- The task can be completed directly in one or two simple steps.\n- You would only create a todo list to restate the user's request.\nHOW TO USE write_todos:\n- Write concrete, self-contained tasks, not vague phases.\n- Assign difficulty to every task: low for simple mechanical or localized work, medium for normal implementation or review requiring judgment, high for complex, broad, ambiguous, or high-risk work.\n- Mark the first active task, or independent active tasks, as in_progress immediately.\n- For sub-agent-worthy work, create one todo per bounded workstream, then call delegate_subagent separately for that task; pass the same difficulty on the delegate call.\n- Do not delegate the entire user request as one task.\n- Update task status as soon as work completes; do not batch completions at the end.\n- Revise the list when new information changes the plan.\n- Do not call write_todos multiple times in parallel; send one full list update at a time.\n- After all work is done, send the final answer as normal assistant text after the last write_todos call.\n\n\nReply with exactly DRAFT_MARKER to greeting requests.\n\n### Preview chat policy\nThis conversation only runs you. You cannot change your own setup — instructions, model, tools, skills, knowledge, channels, integrations, schedules, name or any other configuration — and you have no tool that can. This holds whether or not the user names you as the owner of the thing: \"add a tool\" and \"connect a Slack channel\" are setup changes too. If the user asks for such a change, say plainly that you cannot make it here, and tell them to ask the AI Assistant, which edits the agent for them. Never claim a setup change was applied."},{"role":"user","content":[{"type":"input_text","text":"Give me the greeting marker."}]}],"tools":[{"type":"function","name":"get_environment","description":"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \"today\", deadlines, or scheduling. Tool always requires an empty object as input.","parameters":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_context","description":"Read context from the n8n_chat integration connection.\n\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\n\nQuery inputs:\n\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\n\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\n\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\n\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\n\nUse this tool for read-only lookup before choosing an action target.","parameters":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}},"queries":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}}},"required":["query"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_action","description":"Take actions in the n8n_chat integration connection.\n\nAvailable actions: respond.\n\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\n\nAction inputs:\n\n- respond: input.message is required. Responds in the latest message context for this integration connection.\n\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\n\nrespond uses the latest message context for this integration connection.\n\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\n\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\n\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\n\nFor fields components, use { type: \"fields\", fields: [{ label: \"Account\", value: \"Acme\" }] }. The key items is also accepted as a fields alias.\n\nFor button components, use { type: \"button\", label: \"Approve\", value: \"approve\" }. If label is omitted, text is used as the button label.\n\nFor radio-style choices, use { type: \"radio_select\", label: \"Next step\", options: [{ label: \"Approve\", value: \"approve\" }] }.\n\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\n\nGeneric card examples:\n\nRadio choice card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Acme Executive Briefing — Next Steps\",\n \"card\": {\n \"title\": \"Acme Corporation — Executive Briefing Next Steps\",\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\n\\nWhat should happen next?\"\n },\n {\n \"type\": \"divider\"\n },\n {\n \"type\": \"radio_select\",\n \"id\": \"next_step_selection\",\n \"label\": \"Next step\",\n \"options\": [\n {\n \"label\": \"Schedule exec briefing prep call with Paul G\",\n \"value\": \"schedule_prep_call\"\n },\n {\n \"label\": \"Draft briefing agenda doc for Mike D\",\n \"value\": \"draft_briefing_doc\"\n }\n ]\n }\n ]\n }\n }\n }\n}\n```\n\nButton card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Approve or revise the briefing draft\",\n \"card\": {\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"Review the briefing draft.\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Approve\",\n \"value\": \"approve\",\n \"style\": \"primary\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Revise\",\n \"value\": \"revise\",\n \"style\": \"default\"\n }\n ]\n }\n }\n }\n}\n```\n\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \"plain_text\", text: \"...\" } or { type: \"mrkdwn\", text: \"...\" }.\n\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\n\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.","parameters":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}},"actions":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}}},"required":["action"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"delegate_subagent","description":"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.","parameters":{"type":"object","properties":{"subAgentId":{"type":"string","minLength":1,"description":"Required. Use \"inline\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task."},"taskName":{"type":"string","minLength":1,"description":"Short human-readable name for this delegated task, e.g. \"research_api\"."},"goal":{"type":"string","minLength":1,"description":"The concrete goal the sub-agent should accomplish."},"context":{"type":"string","description":"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out."},"expectedOutput":{"type":"string","description":"The expected shape or contents of the answer."},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning."}},"required":["subAgentId","taskName","goal"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"write_todos","description":"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.","parameters":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Stable identifier for this task within the current plan."},"content":{"type":"string","minLength":1,"description":"Concrete, self-contained task description."},"status":{"type":"string","enum":["pending","in_progress","completed","blocked","cancelled"]},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Task difficulty: \"low\", \"medium\", or \"high\". Use the same value when delegating this task with delegate_subagent."},"delegateHint":{"type":"object","properties":{"subAgentId":{"type":"string","description":"Optional sub-agent id when this task is a delegate_subagent candidate. Use \"inline\" for one-off inline sub-agents."},"expectedOutput":{"type":"string","description":"Optional expected output shape when delegating this task."}},"additionalProperties":false}},"required":["id","content","status","difficulty"],"additionalProperties":false},"description":"Full task list for the current run. Replaces any previous list."}},"required":["todos"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"tool_choice":"auto","stream":true}, + "status": 200, + "response": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0e5e65a777e98645006ab433a7811887d28292e359c4522d04\",\"object\":\"response\",\"created_at\":1790194599,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0e5e65a777e98645006ab433a7811887d28292e359c4522d04\",\"object\":\"response\",\"created_at\":1790194599,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"D\",\"item_id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"logprobs\":[],\"obfuscation\":\"0PNXtJhpCKOv1bX\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"RAFT\",\"item_id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"logprobs\":[],\"obfuscation\":\"yhtHlQCKEZhY\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"_MARK\",\"item_id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"logprobs\":[],\"obfuscation\":\"JSzvnOCANW4\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ER\",\"item_id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"logprobs\":[],\"obfuscation\":\"qWFqSz6xG5W4sf\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":8,\"text\":\"DRAFT_MARKER\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"DRAFT_MARKER\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"DRAFT_MARKER\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0e5e65a777e98645006ab433a7811887d28292e359c4522d04\",\"object\":\"response\",\"created_at\":1790194599,\"status\":\"completed\",\"access_programs\":null,\"background\":false,\"completed_at\":1790194600,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_0e5e65a777e98645006ab433a80bb487d2a1ec484fb2f09cac\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"DRAFT_MARKER\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":2700,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":6,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":2706},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3a762e7d58b3-MAN","connection":"keep-alive","content-type":"text/event-stream; charset=utf-8","date":"Wed, 23 Sep 2026 20:16:40 GMT","openai-processing-ms":"535","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","transfer-encoding":"chunked","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149997082","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"1ms","x-request-id":"req_d5fd298ef86b4ae29288e7ce1206dc04"}, + "responseIsBinary": false + }, + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"Generate a short title and a single emoji for a conversation based on the user message.\nRespond with ONLY a JSON object: {\"title\": \"...\", \"emoji\": \"...\"}\nRules:\n- Title: 2 to 6 words, max 50 characters, sentence case\n- Title must not contain emoji, quotes, colons, or markdown formatting\n- Emoji: exactly one emoji that represents the topic\n- Respond with the JSON object only, no other text"},{"role":"user","content":[{"type":"input_text","text":"\nHere's the conversation:\n\nGive me the greeting marker.\n"}]}]}, + "status": 200, + "response": "{\n \"id\": \"resp_0c77553d6e0766fb006ab433a88de487d28be1f0015050ef57\",\n \"object\": \"response\",\n \"created_at\": 1790194600,\n \"status\": \"completed\",\n \"access_programs\": null,\n \"background\": false,\n \"billing\": {\n \"payer\": \"developer\"\n },\n \"completed_at\": 1790194601,\n \"error\": null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n \"id\": \"msg_0c77553d6e0766fb006ab433a9824887d29423d862d11c7a76\",\n \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": [],\n \"text\": \"{\\\"title\\\":\\\"Request for greeting marker\\\",\\\"emoji\\\":\\\"\\ud83d\\udc4b\\\"}\"\n }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": \"in_memory\",\n \"reasoning\": {\n \"context\": null,\n \"effort\": null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tool_usage\": {\n \"image_gen\": {\n \"input_tokens\": 0,\n \"input_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"output_tokens\": 0,\n \"output_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"total_tokens\": 0\n },\n \"web_search\": {\n \"num_requests\": 0\n }\n },\n \"tools\": [],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": 120,\n \"input_tokens_details\": {\n \"cache_write_tokens\": 0,\n \"cached_tokens\": 0\n },\n \"output_tokens\": 14,\n \"output_tokens_details\": {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 134\n },\n \"user\": null,\n \"metadata\": {}\n}", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3a7ca81f58b3-MAN","connection":"keep-alive","content-type":"application/json","date":"Wed, 23 Sep 2026 20:16:42 GMT","openai-processing-ms":"1428","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149999860","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"0s","x-request-id":"req_615d5d533d52499c9708a039a8ee3c0f"}, + "responseIsBinary": false + } +] diff --git a/packages/cli/test/integration/agents/cassettes/n8n-chat-production/suspends-a-tool-call-and-resumes-it-only-for-the-session-owner.json b/packages/cli/test/integration/agents/cassettes/n8n-chat-production/suspends-a-tool-call-and-resumes-it-only-for-the-session-owner.json new file mode 100644 index 00000000000..3be7ce2fe50 --- /dev/null +++ b/packages/cli/test/integration/agents/cassettes/n8n-chat-production/suspends-a-tool-call-and-resumes-it-only-for-the-session-owner.json @@ -0,0 +1,32 @@ +[ + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 10 child sub-agent runs can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n- write_todos helps you plan and track complex objectives before and during execution. It updates the current task list only; it does not complete tasks, run sub-agents, or answer the user.\nWHEN TO USE write_todos:\n- The user request has 3+ meaningful steps or multiple deliverables.\n- The request decomposes into 2+ independent workstreams.\n- Some workstreams are good candidates for delegate_subagent.\n- You need to track progress, revise the plan, or avoid losing context.\nWHEN NOT TO USE write_todos:\n- The request is trivial, conversational, or informational.\n- The task can be completed directly in one or two simple steps.\n- You would only create a todo list to restate the user's request.\nHOW TO USE write_todos:\n- Write concrete, self-contained tasks, not vague phases.\n- Assign difficulty to every task: low for simple mechanical or localized work, medium for normal implementation or review requiring judgment, high for complex, broad, ambiguous, or high-risk work.\n- Mark the first active task, or independent active tasks, as in_progress immediately.\n- For sub-agent-worthy work, create one todo per bounded workstream, then call delegate_subagent separately for that task; pass the same difficulty on the delegate call.\n- Do not delegate the entire user request as one task.\n- Update task status as soon as work completes; do not batch completions at the end.\n- Revise the list when new information changes the plan.\n- Do not call write_todos multiple times in parallel; send one full list update at a time.\n- After all work is done, send the final answer as normal assistant text after the last write_todos call.\n\n\nWhen asked to record a marker, call approve_marker with the exact marker. Then confirm the tool result."},{"role":"user","content":[{"type":"input_text","text":"Record marker A42 using approve_marker."}]}],"tools":[{"type":"function","name":"approve_marker","description":"Approve a marker before recording it","parameters":{"type":"object","properties":{"marker":{"type":"string"}},"required":["marker"]}},{"type":"function","name":"get_environment","description":"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \"today\", deadlines, or scheduling. Tool always requires an empty object as input.","parameters":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_context","description":"Read context from the n8n_chat integration connection.\n\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\n\nQuery inputs:\n\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\n\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\n\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\n\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\n\nUse this tool for read-only lookup before choosing an action target.","parameters":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}},"queries":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}}},"required":["query"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_action","description":"Take actions in the n8n_chat integration connection.\n\nAvailable actions: respond.\n\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\n\nAction inputs:\n\n- respond: input.message is required. Responds in the latest message context for this integration connection.\n\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\n\nrespond uses the latest message context for this integration connection.\n\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\n\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\n\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\n\nFor fields components, use { type: \"fields\", fields: [{ label: \"Account\", value: \"Acme\" }] }. The key items is also accepted as a fields alias.\n\nFor button components, use { type: \"button\", label: \"Approve\", value: \"approve\" }. If label is omitted, text is used as the button label.\n\nFor radio-style choices, use { type: \"radio_select\", label: \"Next step\", options: [{ label: \"Approve\", value: \"approve\" }] }.\n\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\n\nGeneric card examples:\n\nRadio choice card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Acme Executive Briefing — Next Steps\",\n \"card\": {\n \"title\": \"Acme Corporation — Executive Briefing Next Steps\",\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\n\\nWhat should happen next?\"\n },\n {\n \"type\": \"divider\"\n },\n {\n \"type\": \"radio_select\",\n \"id\": \"next_step_selection\",\n \"label\": \"Next step\",\n \"options\": [\n {\n \"label\": \"Schedule exec briefing prep call with Paul G\",\n \"value\": \"schedule_prep_call\"\n },\n {\n \"label\": \"Draft briefing agenda doc for Mike D\",\n \"value\": \"draft_briefing_doc\"\n }\n ]\n }\n ]\n }\n }\n }\n}\n```\n\nButton card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Approve or revise the briefing draft\",\n \"card\": {\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"Review the briefing draft.\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Approve\",\n \"value\": \"approve\",\n \"style\": \"primary\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Revise\",\n \"value\": \"revise\",\n \"style\": \"default\"\n }\n ]\n }\n }\n }\n}\n```\n\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \"plain_text\", text: \"...\" } or { type: \"mrkdwn\", text: \"...\" }.\n\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\n\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.","parameters":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}},"actions":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}}},"required":["action"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"delegate_subagent","description":"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.","parameters":{"type":"object","properties":{"subAgentId":{"type":"string","minLength":1,"description":"Required. Use \"inline\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task."},"taskName":{"type":"string","minLength":1,"description":"Short human-readable name for this delegated task, e.g. \"research_api\"."},"goal":{"type":"string","minLength":1,"description":"The concrete goal the sub-agent should accomplish."},"context":{"type":"string","description":"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out."},"expectedOutput":{"type":"string","description":"The expected shape or contents of the answer."},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning."}},"required":["subAgentId","taskName","goal"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"write_todos","description":"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.","parameters":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Stable identifier for this task within the current plan."},"content":{"type":"string","minLength":1,"description":"Concrete, self-contained task description."},"status":{"type":"string","enum":["pending","in_progress","completed","blocked","cancelled"]},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Task difficulty: \"low\", \"medium\", or \"high\". Use the same value when delegating this task with delegate_subagent."},"delegateHint":{"type":"object","properties":{"subAgentId":{"type":"string","description":"Optional sub-agent id when this task is a delegate_subagent candidate. Use \"inline\" for one-off inline sub-agents."},"expectedOutput":{"type":"string","description":"Optional expected output shape when delegating this task."}},"additionalProperties":false}},"required":["id","content","status","difficulty"],"additionalProperties":false},"description":"Full task list for the current run. Replaces any previous list."}},"required":["todos"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"tool_choice":"auto","stream":true}, + "status": 200, + "response": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_00273462e81f002f006ab433aa570487d28109ea2524ddb2f6\",\"object\":\"response\",\"created_at\":1790194602,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_00273462e81f002f006ab433aa570487d28109ea2524ddb2f6\",\"object\":\"response\",\"created_at\":1790194602,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_Kbj0Mkj4xg873yySYMKeODPu\",\"name\":\"approve_marker\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"obfuscation\":\"DpE5I5thaZccLQ\",\"output_index\":0,\"sequence_number\":3}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"marker\",\"item_id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"obfuscation\":\"9G1yR4CS5w\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"obfuscation\":\"z4Wk6B2hszACz\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"A\",\"item_id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"obfuscation\":\"vQH0g65lUxjnSpF\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"42\",\"item_id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"obfuscation\":\"CSLVHG36Uqsyk6\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"obfuscation\":\"5IHNXtYSKmo8br\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"marker\\\":\\\"A42\\\"}\",\"item_id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"output_index\":0,\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"marker\\\":\\\"A42\\\"}\",\"call_id\":\"call_Kbj0Mkj4xg873yySYMKeODPu\",\"name\":\"approve_marker\"},\"output_index\":0,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_00273462e81f002f006ab433aa570487d28109ea2524ddb2f6\",\"object\":\"response\",\"created_at\":1790194602,\"status\":\"completed\",\"access_programs\":null,\"background\":false,\"completed_at\":1790194602,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"fc_00273462e81f002f006ab433aae6c887d28a46ffdb99050e4c\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"marker\\\":\\\"A42\\\"}\",\"call_id\":\"call_Kbj0Mkj4xg873yySYMKeODPu\",\"name\":\"approve_marker\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":2611,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":17,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":2628},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3a87ffb758b3-MAN","connection":"keep-alive","content-type":"text/event-stream; charset=utf-8","date":"Wed, 23 Sep 2026 20:16:42 GMT","openai-processing-ms":"504","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","transfer-encoding":"chunked","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149997172","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"1ms","x-request-id":"req_0827926c13e94356aab501a2e9a688e2"}, + "responseIsBinary": false + }, + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 10 child sub-agent runs can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Provider-defined tools are loaded for the inline child's selected model provider.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n- write_todos helps you plan and track complex objectives before and during execution. It updates the current task list only; it does not complete tasks, run sub-agents, or answer the user.\nWHEN TO USE write_todos:\n- The user request has 3+ meaningful steps or multiple deliverables.\n- The request decomposes into 2+ independent workstreams.\n- Some workstreams are good candidates for delegate_subagent.\n- You need to track progress, revise the plan, or avoid losing context.\nWHEN NOT TO USE write_todos:\n- The request is trivial, conversational, or informational.\n- The task can be completed directly in one or two simple steps.\n- You would only create a todo list to restate the user's request.\nHOW TO USE write_todos:\n- Write concrete, self-contained tasks, not vague phases.\n- Assign difficulty to every task: low for simple mechanical or localized work, medium for normal implementation or review requiring judgment, high for complex, broad, ambiguous, or high-risk work.\n- Mark the first active task, or independent active tasks, as in_progress immediately.\n- For sub-agent-worthy work, create one todo per bounded workstream, then call delegate_subagent separately for that task; pass the same difficulty on the delegate call.\n- Do not delegate the entire user request as one task.\n- Update task status as soon as work completes; do not batch completions at the end.\n- Revise the list when new information changes the plan.\n- Do not call write_todos multiple times in parallel; send one full list update at a time.\n- After all work is done, send the final answer as normal assistant text after the last write_todos call.\n\n\nWhen asked to record a marker, call approve_marker with the exact marker. Then confirm the tool result."},{"role":"user","content":[{"type":"input_text","text":"Record marker A42 using approve_marker."}]},{"type":"function_call","call_id":"call_Kbj0Mkj4xg873yySYMKeODPu","name":"approve_marker","arguments":"{\"marker\":\"A42\"}"},{"type":"function_call_output","call_id":"call_Kbj0Mkj4xg873yySYMKeODPu","output":"{\"recorded\":\"A42\"}"}],"tools":[{"type":"function","name":"approve_marker","description":"Approve a marker before recording it","parameters":{"type":"object","properties":{"marker":{"type":"string"}},"required":["marker"]}},{"type":"function","name":"get_environment","description":"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \"today\", deadlines, or scheduling. Tool always requires an empty object as input.","parameters":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_context","description":"Read context from the n8n_chat integration connection.\n\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\n\nQuery inputs:\n\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\n\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\n\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\n\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\n\nUse this tool for read-only lookup before choosing an action target.","parameters":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}},"queries":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string","enum":["get_current_message_context","get_current_subject","get_current_user"]},"input":{"type":"object","additionalProperties":{}}},"required":["query"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"chat_action","description":"Take actions in the n8n_chat integration connection.\n\nAvailable actions: respond.\n\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\n\nAction inputs:\n\n- respond: input.message is required. Responds in the latest message context for this integration connection.\n\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\n\nrespond uses the latest message context for this integration connection.\n\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\n\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\n\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\n\nFor fields components, use { type: \"fields\", fields: [{ label: \"Account\", value: \"Acme\" }] }. The key items is also accepted as a fields alias.\n\nFor button components, use { type: \"button\", label: \"Approve\", value: \"approve\" }. If label is omitted, text is used as the button label.\n\nFor radio-style choices, use { type: \"radio_select\", label: \"Next step\", options: [{ label: \"Approve\", value: \"approve\" }] }.\n\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\n\nGeneric card examples:\n\nRadio choice card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Acme Executive Briefing — Next Steps\",\n \"card\": {\n \"title\": \"Acme Corporation — Executive Briefing Next Steps\",\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\n\\nWhat should happen next?\"\n },\n {\n \"type\": \"divider\"\n },\n {\n \"type\": \"radio_select\",\n \"id\": \"next_step_selection\",\n \"label\": \"Next step\",\n \"options\": [\n {\n \"label\": \"Schedule exec briefing prep call with Paul G\",\n \"value\": \"schedule_prep_call\"\n },\n {\n \"label\": \"Draft briefing agenda doc for Mike D\",\n \"value\": \"draft_briefing_doc\"\n }\n ]\n }\n ]\n }\n }\n }\n}\n```\n\nButton card:\n```json\n{\n \"action\": \"respond\",\n \"input\": {\n \"message\": {\n \"text\": \"Approve or revise the briefing draft\",\n \"card\": {\n \"components\": [\n {\n \"type\": \"section\",\n \"text\": \"Review the briefing draft.\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Approve\",\n \"value\": \"approve\",\n \"style\": \"primary\"\n },\n {\n \"type\": \"button\",\n \"label\": \"Revise\",\n \"value\": \"revise\",\n \"style\": \"default\"\n }\n ]\n }\n }\n }\n}\n```\n\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \"plain_text\", text: \"...\" } or { type: \"mrkdwn\", text: \"...\" }.\n\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\n\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.","parameters":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}},"actions":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["respond"]},"input":{"type":"object","additionalProperties":{}}},"required":["action"],"additionalProperties":false},"minItems":1,"maxItems":20}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"delegate_subagent","description":"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.","parameters":{"type":"object","properties":{"subAgentId":{"type":"string","minLength":1,"description":"Required. Use \"inline\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task."},"taskName":{"type":"string","minLength":1,"description":"Short human-readable name for this delegated task, e.g. \"research_api\"."},"goal":{"type":"string","minLength":1,"description":"The concrete goal the sub-agent should accomplish."},"context":{"type":"string","description":"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out."},"expectedOutput":{"type":"string","description":"The expected shape or contents of the answer."},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning."}},"required":["subAgentId","taskName","goal"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"type":"function","name":"write_todos","description":"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.","parameters":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Stable identifier for this task within the current plan."},"content":{"type":"string","minLength":1,"description":"Concrete, self-contained task description."},"status":{"type":"string","enum":["pending","in_progress","completed","blocked","cancelled"]},"difficulty":{"type":"string","enum":["low","medium","high"],"description":"Task difficulty: \"low\", \"medium\", or \"high\". Use the same value when delegating this task with delegate_subagent."},"delegateHint":{"type":"object","properties":{"subAgentId":{"type":"string","description":"Optional sub-agent id when this task is a delegate_subagent candidate. Use \"inline\" for one-off inline sub-agents."},"expectedOutput":{"type":"string","description":"Optional expected output shape when delegating this task."}},"additionalProperties":false}},"required":["id","content","status","difficulty"],"additionalProperties":false},"description":"Full task list for the current run. Replaces any previous list."}},"required":["todos"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"tool_choice":"auto","stream":true}, + "status": 200, + "response": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0ee12346a33d4462006ab433abab0487d2a882637d2104f2d9\",\"object\":\"response\",\"created_at\":1790194603,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0ee12346a33d4462006ab433abab0487d2a882637d2104f2d9\",\"object\":\"response\",\"created_at\":1790194603,\"status\":\"in_progress\",\"access_programs\":null,\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"The\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"U1atBmpnNITNi\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" marker\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"ZtFEkluN0\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" A\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"dviYZYGKZSbd6P\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"42\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"MxgcYdqY3z3D1J\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" has\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"50DMzM5EuYkI\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" been\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"bHywtgenkXy\",\"output_index\":0,\"sequence_number\":9}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" successfully\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"u85\",\"output_index\":0,\"sequence_number\":10}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" recorded\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"UXP0AAG\",\"output_index\":0,\"sequence_number\":11}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"obfuscation\":\"pwLSdd3UdYlsfmY\",\"output_index\":0,\"sequence_number\":12}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":13,\"text\":\"The marker A42 has been successfully recorded.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The marker A42 has been successfully recorded.\"},\"sequence_number\":14}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The marker A42 has been successfully recorded.\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":15}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0ee12346a33d4462006ab433abab0487d2a882637d2104f2d9\",\"object\":\"response\",\"created_at\":1790194603,\"status\":\"completed\",\"access_programs\":null,\"background\":false,\"completed_at\":1790194604,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_0ee12346a33d4462006ab433ac4c3887d289ada5ec465f99d6\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The marker A42 has been successfully recorded.\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Approve a marker before recording it\",\"name\":\"approve_marker\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Returns runtime info that the LLM cannot know on its own: current ISO date/time, instance timezone (IANA), and day of week. Call when reasoning about \\\"today\\\", deadlines, or scheduling. Tool always requires an empty object as input.\",\"name\":\"get_environment\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"required\":[]},\"strict\":true},{\"type\":\"function\",\"description\":\"Read context from the n8n_chat integration connection.\\n\\nAvailable queries: get_current_message_context, get_current_subject, get_current_user.\\n\\nQuery inputs:\\n\\n- get_current_message_context: no input. Returns the latest place this agent communicated in this thread and any allow-listed platformMessage metadata. For Slack, context.agentUserId is the bot user ID for this agent; do not look it up as another user.\\n\\n- get_current_subject: no input. Returns the subject of the latest message context, such as a Linear issue, when available.\\n\\n- get_current_user: no input. Returns the latest user who interacted in the current message context.\\n\\nBatch form: pass queries as an array of up to 20 { query, input } objects to fetch multiple pieces of context in one tool call.\\n\\nUse this tool for read-only lookup before choosing an action target.\",\"name\":\"chat_context\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"queries\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"enum\":[\"get_current_message_context\",\"get_current_subject\",\"get_current_user\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"query\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Take actions in the n8n_chat integration connection.\\n\\nAvailable actions: respond.\\n\\nThis is the built-in n8n chat: your normal assistant reply already reaches the user. NEVER call respond with only message.text — write that text directly in your reply instead. Call this tool only with message.card, to render a rich card or collect structured input.\\n\\nAction inputs:\\n\\n- respond: input.message is required. Responds in the latest message context for this integration connection.\\n\\nBatch form: pass actions as an array of up to 20 { action, input } objects. Batch actions run sequentially and cannot include cards that wait for a user response.\\n\\nrespond uses the latest message context for this integration connection.\\n\\nNever state that an action has been performed before its tool call returns. Before calling an action, describe what you intend to do, not a completed outcome. Only report an action as done after the tool returns ok. If a tool fails, report the failure honestly.\\n\\nIf this turn was triggered by an inbound chat message, your final reply text is posted to that conversation automatically — do not duplicate it with a text-only respond. Call respond only with message.card, or after switching to a different target.\\n\\nUse message.card for cards, images, key-value summaries, and feedback requests. Include components such as section, fields, image, divider, button, select, or radio_select.\\n\\nFor fields components, use { type: \\\"fields\\\", fields: [{ label: \\\"Account\\\", value: \\\"Acme\\\" }] }. The key items is also accepted as a fields alias.\\n\\nFor button components, use { type: \\\"button\\\", label: \\\"Approve\\\", value: \\\"approve\\\" }. If label is omitted, text is used as the button label.\\n\\nFor radio-style choices, use { type: \\\"radio_select\\\", label: \\\"Next step\\\", options: [{ label: \\\"Approve\\\", value: \\\"approve\\\" }] }.\\n\\nUse only the generic shape: message.text plus optional message.card. Do not provide platform-native component payloads, formatted text objects, or attachments; rendering is handled internally.\\n\\nGeneric card examples:\\n\\nRadio choice card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Acme Executive Briefing — Next Steps\\\",\\n \\\"card\\\": {\\n \\\"title\\\": \\\"Acme Corporation — Executive Briefing Next Steps\\\",\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"30X Expansion Briefing ($3.75M) — Internal owner: Paul G | Briefing contact: Mike D\\\\n\\\\nWhat should happen next?\\\"\\n },\\n {\\n \\\"type\\\": \\\"divider\\\"\\n },\\n {\\n \\\"type\\\": \\\"radio_select\\\",\\n \\\"id\\\": \\\"next_step_selection\\\",\\n \\\"label\\\": \\\"Next step\\\",\\n \\\"options\\\": [\\n {\\n \\\"label\\\": \\\"Schedule exec briefing prep call with Paul G\\\",\\n \\\"value\\\": \\\"schedule_prep_call\\\"\\n },\\n {\\n \\\"label\\\": \\\"Draft briefing agenda doc for Mike D\\\",\\n \\\"value\\\": \\\"draft_briefing_doc\\\"\\n }\\n ]\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nButton card:\\n```json\\n{\\n \\\"action\\\": \\\"respond\\\",\\n \\\"input\\\": {\\n \\\"message\\\": {\\n \\\"text\\\": \\\"Approve or revise the briefing draft\\\",\\n \\\"card\\\": {\\n \\\"components\\\": [\\n {\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": \\\"Review the briefing draft.\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Approve\\\",\\n \\\"value\\\": \\\"approve\\\",\\n \\\"style\\\": \\\"primary\\\"\\n },\\n {\\n \\\"type\\\": \\\"button\\\",\\n \\\"label\\\": \\\"Revise\\\",\\n \\\"value\\\": \\\"revise\\\",\\n \\\"style\\\": \\\"default\\\"\\n }\\n ]\\n }\\n }\\n }\\n}\\n```\\n\\nNever send message.blocks, components of type actions, elements arrays, action_id, radio_buttons, or formatted text objects such as { type: \\\"plain_text\\\", text: \\\"...\\\" } or { type: \\\"mrkdwn\\\", text: \\\"...\\\" }.\\n\\nInteractive message.card components (button, select, or radio_select) send the message first, then suspend this action until the user responds.\\n\\nDisplay-only message.card components without buttons/selects render the card and let the agent continue immediately.\",\"name\":\"chat_action\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}},\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"respond\"]},\"input\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"action\"],\"additionalProperties\":false},\"minItems\":1,\"maxItems\":20}},\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a bounded, self-contained subtask to a focused child agent that runs in an isolated context and returns only a concise final result. Use it for reasoning-heavy subtasks, context-flooding investigations, or independent workstreams inside a larger deliverable. Do not use it for trivial work, single tool calls, mechanical steps, tasks that need hidden conversation context, or pass-through delegation of the entire user request.\",\"name\":\"delegate_subagent\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Required. Use \\\"inline\\\" for a one-off inline sub-agent. Use an exact configured sub-agent ID only when one is listed and fits the task.\"},\"taskName\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Short human-readable name for this delegated task, e.g. \\\"research_api\\\".\"},\"goal\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The concrete goal the sub-agent should accomplish.\"},\"context\":{\"type\":\"string\",\"description\":\"All details the child needs, since it sees nothing else: constraints, paths, data, prior decisions, acceptance criteria, and what you have already tried or ruled out.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"The expected shape or contents of the answer.\"},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Optional difficulty estimate for this delegated task. Use low for simple bounded work, medium for moderate analysis or implementation, and high for complex research, architecture, or multi-step reasoning.\"}},\"required\":[\"subAgentId\",\"taskName\",\"goal\",\"context\",\"expectedOutput\",\"difficulty\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Create or update a structured task list for complex agent work. Use it to decompose a larger request into concrete workstreams, track progress, and identify which tasks should be handled separately with delegate_subagent. Do not use it for trivial work, single-step tasks, or purely conversational answers. This tool only updates the task list; it does not run sub-agents or answer the user.\",\"name\":\"write_todos\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"todos\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Stable identifier for this task within the current plan.\"},\"content\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Concrete, self-contained task description.\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"blocked\",\"cancelled\"]},\"difficulty\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\"],\"description\":\"Task difficulty: \\\"low\\\", \\\"medium\\\", or \\\"high\\\". Use the same value when delegating this task with delegate_subagent.\"},\"delegateHint\":{\"type\":\"object\",\"properties\":{\"subAgentId\":{\"type\":\"string\",\"description\":\"Optional sub-agent id when this task is a delegate_subagent candidate. Use \\\"inline\\\" for one-off inline sub-agents.\"},\"expectedOutput\":{\"type\":\"string\",\"description\":\"Optional expected output shape when delegating this task.\"}},\"additionalProperties\":false,\"required\":[\"subAgentId\",\"expectedOutput\"]}},\"required\":[\"id\",\"content\",\"status\",\"difficulty\",\"delegateHint\"],\"additionalProperties\":false},\"description\":\"Full task list for the current run. Replaces any previous list.\"}},\"required\":[\"todos\"],\"additionalProperties\":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":2641,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":11,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":2652},\"user\":null,\"metadata\":{}},\"sequence_number\":16}\n\n", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3a904e8f58b3-MAN","connection":"keep-alive","content-type":"text/event-stream; charset=utf-8","date":"Wed, 23 Sep 2026 20:16:44 GMT","openai-processing-ms":"656","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","transfer-encoding":"chunked","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149997142","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"1ms","x-request-id":"req_b1b542975df0451782117c25f1858ca0"}, + "responseIsBinary": false + }, + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/responses", + "body": {"model":"gpt-4o-mini","input":[{"role":"system","content":"Generate a short title and a single emoji for a conversation based on the user message.\nRespond with ONLY a JSON object: {\"title\": \"...\", \"emoji\": \"...\"}\nRules:\n- Title: 2 to 6 words, max 50 characters, sentence case\n- Title must not contain emoji, quotes, colons, or markdown formatting\n- Emoji: exactly one emoji that represents the topic\n- Respond with the JSON object only, no other text"},{"role":"user","content":[{"type":"input_text","text":"\nHere's the conversation:\n\nRecord marker A42 using approve_marker.\n"}]}]}, + "status": 200, + "response": "{\n \"id\": \"resp_0845830b9da5818b006ab433acefc487d2a274ad8d9bf95cb3\",\n \"object\": \"response\",\n \"created_at\": 1790194604,\n \"status\": \"completed\",\n \"access_programs\": null,\n \"background\": false,\n \"billing\": {\n \"payer\": \"developer\"\n },\n \"completed_at\": 1790194605,\n \"error\": null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n \"id\": \"msg_0845830b9da5818b006ab433ad619087d299a061f91b519842\",\n \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": [],\n \"text\": \"{\\\"title\\\":\\\"Record marker A42\\\",\\\"emoji\\\":\\\"\\u2705\\\"}\"\n }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": \"in_memory\",\n \"reasoning\": {\n \"context\": null,\n \"effort\": null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tool_usage\": {\n \"image_gen\": {\n \"input_tokens\": 0,\n \"input_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"output_tokens\": 0,\n \"output_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"total_tokens\": 0\n },\n \"web_search\": {\n \"num_requests\": 0\n }\n },\n \"tools\": [],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": 122,\n \"input_tokens_details\": {\n \"cache_write_tokens\": 0,\n \"cached_tokens\": 0\n },\n \"output_tokens\": 13,\n \"output_tokens_details\": {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 135\n },\n \"user\": null,\n \"metadata\": {}\n}", + "rawHeaders": {"access-control-expose-headers":"X-Request-ID, CF-Ray, CF-Ray","alt-svc":"h3=\":443\"; ma=86400","cf-cache-status":"DYNAMIC","cf-ray":"a3fc3a983ba458b3-MAN","connection":"keep-alive","content-type":"application/json","date":"Wed, 23 Sep 2026 20:16:45 GMT","openai-processing-ms":"893","openai-version":"2020-10-01","server":"cloudflare","strict-transport-security":"max-age=31536000; includeSubDomains; preload","x-content-type-options":"nosniff","x-ratelimit-limit-requests":"30000","x-ratelimit-limit-tokens":"150000000","x-ratelimit-remaining-requests":"29999","x-ratelimit-remaining-tokens":"149999860","x-ratelimit-reset-requests":"2ms","x-ratelimit-reset-tokens":"0s","x-request-id":"req_6b1d07c182d84a5191d2ba30a00418ac"}, + "responseIsBinary": false + } +] diff --git a/packages/cli/test/integration/agents/n8n-chat-production.api.test.ts b/packages/cli/test/integration/agents/n8n-chat-production.api.test.ts new file mode 100644 index 00000000000..6bf5279d8e8 --- /dev/null +++ b/packages/cli/test/integration/agents/n8n-chat-production.api.test.ts @@ -0,0 +1,214 @@ +import { randomUUID } from 'node:crypto'; + +import { linkUserToProject, testModules } from '@n8n/backend-test-utils'; +import { ProjectRepository } from '@n8n/db'; +import { Container } from '@n8n/di'; + +import { AgentRepository } from '@/modules/agents/repositories/agent.repository'; +import { AgentHistoryRepository } from '@/modules/agents/repositories/agent-history.repository'; +import { AgentExecutionThreadRepository } from '@/modules/agents/repositories/agent-execution-thread.repository'; +import { AgentExecutionRepository } from '@/modules/agents/repositories/agent-execution.repository'; + +import { createMember, createOwner } from '../shared/db/users'; +import { setupTestServer } from '../shared/utils'; + +beforeAll(async () => { + await testModules.loadModules(['agents']); +}); + +const server = setupTestServer({ endpointGroups: ['ai'] }); +const chatIntegration = { type: 'n8n_chat', credentialId: '' } as const; + +describe('production n8n Chat HTTP route', () => { + async function createAgent(ownerId: string) { + const project = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(ownerId); + const repository = Container.get(AgentRepository); + const agent = await repository.save( + repository.create({ + id: randomUUID(), + name: 'Agent', + projectId: project.id, + schema: { name: 'Agent', model: 'openai:gpt-4o-mini', instructions: 'Help' }, + integrations: [chatIntegration], + tools: {}, + skills: {}, + versionId: randomUUID(), + }), + ); + return { agent, project }; + } + + async function activateChat(agentId: string, enabled = true) { + const versionId = randomUUID(); + await Container.get(AgentHistoryRepository).saveVersion({ + versionId, + agentId, + schema: { + name: 'Agent', + model: 'openai:gpt-4o-mini', + instructions: 'Help', + integrations: enabled ? [chatIntegration] : [], + }, + tools: {}, + skills: {}, + publishedBy: 'test', + }); + await Container.get(AgentRepository).update({ id: agentId }, { activeVersionId: versionId }); + } + + async function createThread(agentId: string, projectId: string, ownerId: string, source: string) { + const threadRepository = Container.get(AgentExecutionThreadRepository); + const thread = await threadRepository.save( + threadRepository.create({ + id: randomUUID(), + agentId, + agentName: 'Agent', + projectId, + accessScope: 'user', + ownerId, + sessionNumber: 1, + }), + ); + const executionRepository = Container.get(AgentExecutionRepository); + const execution = await executionRepository.save( + executionRepository.create({ + id: randomUUID(), + threadId: thread.id, + status: 'success', + userMessage: 'Hello', + source, + }), + ); + return { thread, execution }; + } + + it('rejects an unpublished agent before a model request', async () => { + const owner = await createOwner(); + const { project, agent } = await createAgent(owner.id); + const response = await server + .authAgentFor(owner) + .post(`/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`) + .send({ message: 'Hello' }) + .expect(200); + expect(response.text).toContain('"errorCode":"agent_unavailable"'); + }); + + it('rejects a published version with n8n Chat disabled', async () => { + const owner = await createOwner(); + const { project, agent } = await createAgent(owner.id); + await activateChat(agent.id, false); + const response = await server + .authAgentFor(owner) + .post(`/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`) + .send({ message: 'Hello' }) + .expect(200); + expect(response.text).toContain('"errorCode":"agent_unavailable"'); + }); + + it('rejects a preview session in the production namespace', async () => { + const owner = await createOwner(); + const { project, agent } = await createAgent(owner.id); + await activateChat(agent.id); + const { thread, execution } = await createThread(agent.id, project.id, owner.id, 'chat'); + const url = `/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`; + await server.authAgentFor(owner).get(`${url}/${thread.id}/messages`).expect(404); + const response = await server + .authAgentFor(owner) + .post(url) + .send({ message: 'Continue', sessionId: thread.id }) + .expect(200); + expect(response.text).toContain('Session not found'); + await server + .authAgentFor(owner) + .delete(`${url}/${thread.id}/executions/${execution.id}`) + .expect(404); + }); + + it('rejects a production session in the preview namespace', async () => { + const owner = await createOwner(); + const { project, agent } = await createAgent(owner.id); + await activateChat(agent.id); + const { thread, execution } = await createThread( + agent.id, + project.id, + owner.id, + 'n8n_chat_production', + ); + const url = `/projects/${project.id}/agents/v2/${agent.id}/chat`; + await server.authAgentFor(owner).get(`${url}/${thread.id}/messages`).expect(404); + const response = await server + .authAgentFor(owner) + .post(url) + .send({ message: 'Continue', sessionId: thread.id }) + .expect(200); + expect(response.text).toContain('Session not found'); + await server + .authAgentFor(owner) + .delete(`${url}/${thread.id}/executions/${execution.id}`) + .expect(404); + const sessionsUrl = `/projects/${project.id}/agents/v2/${agent.id}/threads`; + const previewSessions = await server + .authAgentFor(owner) + .get(`${sessionsUrl}?origin=preview`) + .expect(200); + expect(previewSessions.body.data.threads).toEqual([]); + const productionSessions = await server + .authAgentFor(owner) + .get(`${sessionsUrl}?origin=n8n_chat_production`) + .expect(200); + expect(productionSessions.body.data.threads).toEqual([ + expect.objectContaining({ id: thread.id }), + ]); + const detail = await server.authAgentFor(owner).get(`${sessionsUrl}/${thread.id}`).expect(200); + expect(detail.body.data.thread.canContinueInPreview).toBe(false); + }); + + it('keeps another project member out of a private production session', async () => { + const owner = await createOwner(); + const other = await createMember(); + const { project, agent } = await createAgent(owner.id); + await linkUserToProject(other, project, 'project:editor'); + await activateChat(agent.id); + const { thread, execution } = await createThread( + agent.id, + project.id, + owner.id, + 'n8n_chat_production', + ); + const url = `/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`; + await server.authAgentFor(other).get(`${url}/${thread.id}/messages`).expect(404); + const response = await server + .authAgentFor(other) + .post(url) + .send({ message: 'Continue', sessionId: thread.id }) + .expect(200); + expect(response.text).toContain('Session not found'); + await server + .authAgentFor(other) + .delete(`${url}/${thread.id}/executions/${execution.id}`) + .expect(404); + }); + + it('requires project access before starting production chat', async () => { + const owner = await createOwner(); + const other = await createMember(); + const { project, agent } = await createAgent(owner.id); + await activateChat(agent.id); + await server + .authAgentFor(other) + .post(`/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`) + .send({ message: 'Hello' }) + .expect(403); + }); + + it('removes production reads when the agent is unpublished', async () => { + const owner = await createOwner(); + const { project, agent } = await createAgent(owner.id); + await activateChat(agent.id); + const { thread } = await createThread(agent.id, project.id, owner.id, 'n8n_chat_production'); + const url = `/projects/${project.id}/agents/v2/${agent.id}/n8n-chat/${thread.id}/messages`; + await server.authAgentFor(owner).get(url).expect(200); + await Container.get(AgentRepository).update({ id: agent.id }, { activeVersionId: null }); + await server.authAgentFor(owner).get(url).expect(404); + }); +}); diff --git a/packages/cli/test/integration/agents/n8n-chat-production.model.test.ts b/packages/cli/test/integration/agents/n8n-chat-production.model.test.ts new file mode 100644 index 00000000000..9969eeb9666 --- /dev/null +++ b/packages/cli/test/integration/agents/n8n-chat-production.model.test.ts @@ -0,0 +1,439 @@ +import { randomUUID } from 'node:crypto'; +import { mkdirSync } from 'node:fs'; +import path from 'node:path'; +import { brotliDecompressSync, gunzipSync } from 'node:zlib'; + +import type { ToolDescriptor } from '@n8n/agents'; +import type { AgentJsonConfig } from '@n8n/api-types'; +import { linkUserToProject, testModules } from '@n8n/backend-test-utils'; +import { ProjectRepository } from '@n8n/db'; +import { Container } from '@n8n/di'; +import nock, { back as nockBack } from 'nock'; +import { BinaryDataConfig, BinaryDataService } from 'n8n-core'; + +import type { Agent } from '@/modules/agents/entities/agent.entity'; +import { AgentHistoryRepository } from '@/modules/agents/repositories/agent-history.repository'; +import { AgentExecutionRepository } from '@/modules/agents/repositories/agent-execution.repository'; +import { AgentExecutionThreadRepository } from '@/modules/agents/repositories/agent-execution-thread.repository'; +import { AgentRepository } from '@/modules/agents/repositories/agent.repository'; + +import { saveCredential } from '../shared/db/credentials'; +import { createOwner, createMember } from '../shared/db/users'; +import { initCredentialsTypes, setupTestServer } from '../shared/utils'; + +vi.mock('@/utils/ai-proxy-fetch', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + createAiProxyFetch: () => async (input: RequestInfo | URL, init?: RequestInit) => + await globalThis.fetch(input, init), + }; +}); + +const enabled = process.env.AGENT_N8N_CHAT_MODEL_TESTS === '1' || Boolean(process.env.CI); +const record = process.env.VCR_MODE === 'record'; +const replay = !record && (process.env.VCR_MODE === 'replay' || Boolean(process.env.CI)); +const cassettes = path.join(__dirname, 'cassettes', 'n8n-chat-production'); +const chatIntegration = { type: 'n8n_chat', credentialId: '' } as const; +const redPixel = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'; + +nock.restore(); +const directFetch = globalThis.fetch; + +function bypassLocalFetchInterceptors() { + const interceptedFetch = globalThis.fetch; + Object.defineProperty(globalThis, 'fetch', { + value: async (input: RequestInfo | URL, init?: RequestInit) => + await (/localhost|127\.0\.0\.1/.test(input.toString()) + ? directFetch(input, init) + : interceptedFetch(input, init)), + configurable: true, + }); +} + +const approvalToolCode = ` +import { Tool } from '@n8n/agents'; +import { z } from 'zod'; + +export default new Tool('approve_marker') + .description('Approve a marker before recording it') + .input(z.object({ marker: z.string() })) + .handler(async ({ marker }) => ({ recorded: marker })); +`; + +const approvalToolDescriptor: ToolDescriptor = { + name: 'approve_marker', + description: 'Approve a marker before recording it', + systemInstruction: null, + inputSchema: { + type: 'object', + properties: { marker: { type: 'string' } }, + required: ['marker'], + }, + outputSchema: null, + hasSuspend: false, + hasResume: false, + hasToMessage: false, + requireApproval: false, + providerOptions: null, +}; + +function events(body: string): Array> { + return body + .split('\n\n') + .filter((block) => block.startsWith('data: ')) + .map((block) => JSON.parse(block.slice(6)) as Record); +} + +function eventOfType(items: Array>, type: string) { + return items.find((event) => event.type === type); +} + +function streamedText(items: Array>): string { + return items + .filter((event) => event.type === 'text-delta') + .map((event) => (typeof event.delta === 'string' ? event.delta : '')) + .join(''); +} + +function scrubCassette(definitions: nock.Definition[]): nock.Definition[] { + return definitions + .filter((definition) => !/localhost|127\.0\.0\.1|models\.dev/.test(String(definition.scope))) + .map((definition) => { + const headers = definition.rawHeaders; + if ( + headers?.['content-type']?.includes('application/json') && + (headers['content-encoding'] === 'br' || headers['content-encoding'] === 'gzip') && + Array.isArray(definition.response) && + typeof definition.response[0] === 'string' + ) { + const bytes = Buffer.from(definition.response[0], 'hex'); + definition.response = + headers['content-encoding'] === 'br' + ? brotliDecompressSync(bytes).toString('utf8') + : gunzipSync(bytes).toString('utf8'); + for (const header of ['content-encoding', 'content-length', 'transfer-encoding']) { + delete headers[header]; + } + } + for (const header of ['authorization', 'api-key', 'x-api-key']) { + delete definition.reqheaders?.[header]; + } + for (const header of ['set-cookie', 'openai-organization', 'openai-project']) { + delete definition.rawHeaders?.[header]; + } + return definition; + }); +} + +function formatCassette(definitions: nock.Definition[]): string { + return `[ +${definitions + .map((definition) => { + const fields = Object.entries(definition) + .map(([key, value]) => `\t\t${JSON.stringify(key)}: ${JSON.stringify(value)}`) + .join(',\n'); + return `\t{\n${fields}\n\t}`; + }) + .join(',\n')} +]\n`; +} + +beforeAll(async () => { + await testModules.loadModules(['agents']); +}); + +const server = setupTestServer({ endpointGroups: ['ai'] }); + +describe.skipIf(!enabled)('production n8n Chat with a real model', () => { + let finishCassette: (() => void) | undefined; + let fetchBeforeCassette: typeof globalThis.fetch | undefined; + + beforeAll(async () => { + await initCredentialsTypes(); + const testFolder = process.env.N8N_USER_FOLDER; + if (!testFolder) throw new Error('N8N_USER_FOLDER is required'); + const binaryDataConfig = Container.get(BinaryDataConfig); + binaryDataConfig.mode = 'filesystem'; + binaryDataConfig.localStoragePath = path.join(testFolder, '.n8n', 'storage'); + await Container.get(BinaryDataService).init(); + if (!replay && !process.env.OPENAI_API_KEY) { + throw new Error('Set OPENAI_API_KEY for live or record mode'); + } + nockBack.fixtures = cassettes; + if (record) mkdirSync(cassettes, { recursive: true }); + nockBack.setMode(record ? 'update' : replay ? 'lockdown' : 'wild'); + if (replay) process.env.OPENAI_API_KEY = 'sk-test-replay'; + }); + + beforeEach(async (context) => { + if (!record && !replay) return; + if (!nock.isActive()) nock.activate(); + const name = `${context.task.name.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.json`; + const cassette = await nockBack(name, { + afterRecord: (definitions) => formatCassette(scrubCassette(definitions)), + }); + finishCassette = cassette.nockDone; + fetchBeforeCassette = globalThis.fetch; + bypassLocalFetchInterceptors(); + nock.enableNetConnect(record ? /127\.0\.0\.1|api\.openai\.com/ : /127\.0\.0\.1/); + nock('https://models.dev') + .get('/api.json') + .reply(200, { + openai: { + id: 'openai', + name: 'OpenAI', + models: { + 'gpt-4o-mini': { + id: 'gpt-4o-mini', + name: 'GPT-4o mini', + tool_call: true, + limit: { context: 128000, output: 16384 }, + }, + }, + }, + }) + .persist(); + }); + + afterEach(() => { + finishCassette?.(); + finishCassette = undefined; + if (fetchBeforeCassette) { + Object.defineProperty(globalThis, 'fetch', { + value: fetchBeforeCassette, + configurable: true, + }); + fetchBeforeCassette = undefined; + } + nock.cleanAll(); + nock.restore(); + }); + + async function createPublishedAgent(options: { approvalTool?: boolean; instructions: string }) { + const owner = await createOwner(); + const project = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail( + owner.id, + ); + const credential = await saveCredential( + { + name: 'OpenAI', + type: 'openAiApi', + data: { apiKey: process.env.OPENAI_API_KEY ?? 'sk-test-replay' }, + }, + { project, role: 'credential:owner' }, + ); + const schema: AgentJsonConfig = { + name: 'Chat test agent', + model: 'openai/gpt-4o-mini', + credential: credential.id, + instructions: options.instructions, + memory: { enabled: true, storage: 'n8n' }, + ...(options.approvalTool + ? { tools: [{ type: 'custom' as const, id: 'approve_marker', requireApproval: true }] } + : {}), + }; + const tools: Agent['tools'] = options.approvalTool + ? { + approve_marker: { + code: approvalToolCode, + descriptor: approvalToolDescriptor, + }, + } + : {}; + const repository = Container.get(AgentRepository); + const agent = await repository.save( + repository.create({ + id: randomUUID(), + name: schema.name, + projectId: project.id, + schema, + tools, + skills: {}, + integrations: [chatIntegration], + versionId: randomUUID(), + }), + ); + const versionId = randomUUID(); + await Container.get(AgentHistoryRepository).saveVersion({ + versionId, + agentId: agent.id, + schema: { ...schema, integrations: [chatIntegration] }, + tools, + skills: {}, + publishedBy: owner, + }); + await repository.update({ id: agent.id }, { activeVersionId: versionId }); + return { owner, project, agent, versionId }; + } + + it('streams the published instructions and records a private production session', async () => { + const { owner, project, agent } = await createPublishedAgent({ + instructions: 'Reply with exactly PUBLISHED_MARKER to greeting requests.', + }); + const repository = Container.get(AgentRepository); + if (!agent.schema) throw new Error('Agent schema is missing'); + agent.schema = { + ...agent.schema, + instructions: 'Reply with exactly DRAFT_MARKER to greeting requests.', + }; + expect(await repository.saveDraftFenced(agent)).toBe(true); + const response = await server + .authAgentFor(owner) + .post(`/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`) + .send({ message: 'Give me the greeting marker.' }) + .expect(200); + const streamed = events(response.text); + const done = eventOfType(streamed, 'done'); + expect(done).toBeDefined(); + expect(streamedText(streamed)).toContain('PUBLISHED_MARKER'); + expect(streamedText(streamed)).not.toContain('DRAFT_MARKER'); + const threadId = done?.sessionId; + if (typeof threadId !== 'string') throw new Error('Session ID is missing'); + const thread = await Container.get(AgentExecutionThreadRepository).findOneBy({ id: threadId }); + expect(thread).toMatchObject({ ownerId: owner.id, accessScope: 'user' }); + const execution = await Container.get(AgentExecutionRepository).findLatestByThreadId(threadId); + expect(execution).toMatchObject({ source: 'n8n_chat_production' }); + const preview = await server + .authAgentFor(owner) + .post(`/projects/${project.id}/agents/v2/${agent.id}/chat`) + .send({ message: 'Give me the greeting marker.' }) + .expect(200); + const previewEvents = events(preview.text); + const previewDone = eventOfType(previewEvents, 'done'); + expect(previewDone).toBeDefined(); + expect(streamedText(previewEvents)).toContain('DRAFT_MARKER'); + expect(streamedText(previewEvents)).not.toContain('PUBLISHED_MARKER'); + if (typeof previewDone?.sessionId !== 'string') { + throw new Error('Preview session ID is missing'); + } + const previewExecution = await Container.get(AgentExecutionRepository).findLatestByThreadId( + previewDone.sessionId, + ); + // Queued preview turns carry the queue item source, not the production marker. + expect(previewExecution?.source).toBe('chat'); + }, 120_000); + + it('suspends a tool call and resumes it only for the session owner', async () => { + const { owner, project, agent } = await createPublishedAgent({ + approvalTool: true, + instructions: + 'When asked to record a marker, call approve_marker with the exact marker. Then confirm the tool result.', + }); + const base = `/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`; + const response = await server + .authAgentFor(owner) + .post(base) + .send({ message: 'Record marker A42 using approve_marker.' }) + .expect(200); + const streamed = events(response.text); + const suspended = eventOfType(streamed, 'tool-call-suspended'); + expect(suspended).toBeDefined(); + expect(eventOfType(streamed, 'done')).toBeUndefined(); + const payload = suspended?.payload; + if ( + !payload || + typeof payload !== 'object' || + !('runId' in payload) || + !('toolCallId' in payload) || + typeof payload.runId !== 'string' || + typeof payload.toolCallId !== 'string' + ) + throw new Error('Suspended tool call is missing its identifiers'); + const started = eventOfType(streamed, 'execution-started'); + const threadId = started?.sessionId as string; + const other = await createMember(); + await linkUserToProject(other, project, 'project:editor'); + await server.authAgentFor(other).get(`${base}/${threadId}/messages`).expect(404); + const denied = await server + .authAgentFor(other) + .post(`${base}/resume`) + .send({ + runId: payload.runId, + toolCallId: payload.toolCallId, + resumeData: { approved: true }, + }) + .expect(200); + expect(denied.text).toMatch(/does not belong|Session not found/); + const history = await server + .authAgentFor(owner) + .get(`${base}/${threadId}/messages`) + .expect(200); + expect(history.body.data.openSuspensions).toHaveLength(1); + const resumed = await server + .authAgentFor(owner) + .post(`${base}/resume`) + .send({ + runId: payload.runId, + toolCallId: payload.toolCallId, + resumeData: { approved: true }, + }) + .expect(200); + expect(eventOfType(events(resumed.text), 'done')).toBeDefined(); + }, 120_000); + + it('sends an image to the model and reloads the stored attachment', async () => { + const { owner, project, agent } = await createPublishedAgent({ + instructions: 'Describe the color of an attached image in one word.', + }); + const base = `/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`; + const response = await server + .authAgentFor(owner) + .post(base) + .send({ + message: 'What color is this image?', + attachments: [{ fileName: 'pixel.png', mimeType: 'image/png', data: redPixel }], + }) + .expect(200); + const streamed = events(response.text); + const done = eventOfType(streamed, 'done'); + expect(done, JSON.stringify(streamed)).toBeDefined(); + expect(streamedText(streamed)).toMatch(/red/i); + const threadId = done?.sessionId as string; + const history = await server + .authAgentFor(owner) + .get(`${base}/${threadId}/messages`) + .expect(200); + expect(history.body.data.messages).toEqual(expect.any(Array)); + const attachment = await Container.get(AgentExecutionRepository).findLatestByThreadId(threadId); + const attachmentId = attachment?.attachments?.[0]?.id; + expect(attachmentId).toBeDefined(); + const downloaded = await server + .authAgentFor(owner) + .get(`${base}/attachments/${attachmentId}`) + .expect(200); + expect(downloaded.body).toEqual(Buffer.from(redPixel, 'base64')); + }, 120_000); + + it('allows only the session owner to cancel a suspended run', async () => { + const { owner, project, agent } = await createPublishedAgent({ + approvalTool: true, + instructions: 'When asked to record a marker, call approve_marker with that marker.', + }); + const base = `/projects/${project.id}/agents/v2/${agent.id}/n8n-chat`; + const response = await server + .authAgentFor(owner) + .post(base) + .send({ message: 'Record marker B17 using approve_marker.' }) + .expect(200); + const suspension = eventOfType(events(response.text), 'tool-call-suspended'); + const payload = suspension?.payload; + if (!payload || typeof payload !== 'object' || !('runId' in payload)) { + throw new Error('Suspended tool call is missing its run ID'); + } + if (typeof payload.runId !== 'string') throw new Error('Run ID is invalid'); + const other = await createMember(); + await linkUserToProject(other, project, 'project:editor'); + const foreignCancel = await server + .authAgentFor(other) + .delete(`${base}/runs/${payload.runId}`) + .expect(200); + expect(foreignCancel.body.data.cancelled).toBe(false); + const ownerCancel = await server + .authAgentFor(owner) + .delete(`${base}/runs/${payload.runId}`) + .expect(200); + expect(ownerCancel.body.data.cancelled).toBe(true); + }, 120_000); +}); diff --git a/packages/cli/test/integration/database/repositories/agent.repository.test.ts b/packages/cli/test/integration/database/repositories/agent.repository.test.ts index e9f4be0ee9a..c8ab0984748 100644 --- a/packages/cli/test/integration/database/repositories/agent.repository.test.ts +++ b/packages/cli/test/integration/database/repositories/agent.repository.test.ts @@ -59,6 +59,48 @@ describe('AgentRepository', () => { await testDb.terminate(); }); + describe('published n8n Chat availability', () => { + it('uses the active version instead of the mutable draft switch', async () => { + const agent = await createAgent(); + expect(agent.integrations).toEqual([]); + expect(await agentRepo.isN8nChatPublished(agent.id, projectId)).toBe(false); + const firstVersion = uuid(); + await agentHistoryRepo.saveVersion({ + versionId: firstVersion, + agentId: agent.id, + schema: agent.schema + ? { ...agent.schema, integrations: [{ type: 'n8n_chat', credentialId: '' }] } + : null, + tools: {}, + skills: {}, + publishedBy: 'test', + }); + await agentRepo.update({ id: agent.id }, { activeVersionId: firstVersion }); + expect(await agentRepo.isN8nChatPublished(agent.id, projectId)).toBe(true); + await agentRepo.update({ id: agent.id }, { integrations: [] }); + expect(await agentRepo.isN8nChatPublished(agent.id, projectId)).toBe(true); + + const secondVersion = uuid(); + await agentHistoryRepo.saveVersion({ + versionId: secondVersion, + agentId: agent.id, + schema: agent.schema, + tools: {}, + skills: {}, + publishedBy: 'test', + }); + await agentRepo.update({ id: agent.id }, { activeVersionId: secondVersion }); + expect(await agentRepo.isN8nChatPublished(agent.id, projectId)).toBe(false); + await agentRepo.update({ id: agent.id }, { activeVersionId: firstVersion }); + expect(await agentRepo.isN8nChatPublished(agent.id, projectId)).toBe(true); + await agentRepo.update({ id: agent.id }, { activeVersionId: null }); + expect(await agentRepo.isN8nChatPublished(agent.id, projectId)).toBe(false); + expect(await agentRepo.isN8nChatPublished(agent.id, uuid())).toBe(false); + await agentRepo.delete({ id: agent.id }); + expect(await agentRepo.isN8nChatPublished(agent.id, projectId)).toBe(false); + }); + }); + describe('saveDraftFenced', () => { it('persists draft columns and bumps revision when the fence is won', async () => { const agent = await createAgent(); diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentApi.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentApi.test.ts index 7e5da944555..913b11c95d4 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentApi.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentApi.test.ts @@ -215,6 +215,7 @@ describe('useAgentApi', () => { integrations: [ { type: 'telegram', credentialId: 'cred-telegram-1' }, { type: 'slack', credentialId: 'cred-slack-1', settings: { channel: 'C1' } }, + { type: 'n8n_chat', credentialId: '' }, ], } as unknown as AgentJsonConfig; const cloned = { id: 'agent-2', name: 'Support Agent (copy)' } as unknown as AgentResource; @@ -236,6 +237,7 @@ describe('useAgentApi', () => { expect(postBody.schema.integrations).toEqual([ { type: 'telegram', credentialId: '' }, { type: 'slack', credentialId: '', settings: { channel: 'C1' } }, + { type: 'n8n_chat', credentialId: '' }, ]); }); }); diff --git a/packages/frontend/editor-ui/src/features/agents/composables/useAgentApi.ts b/packages/frontend/editor-ui/src/features/agents/composables/useAgentApi.ts index 7327906ee37..c0481f73c1a 100644 --- a/packages/frontend/editor-ui/src/features/agents/composables/useAgentApi.ts +++ b/packages/frontend/editor-ui/src/features/agents/composables/useAgentApi.ts @@ -146,10 +146,9 @@ export const duplicateAgent = async ( // time (and break the source's channel). Blank to drafts so the builder // opens the copy with a "connect a channel" chip instead. const { tasks: _tasks, integrations: sourceIntegrations, ...rest } = configResponse.config; - const draftIntegrations = (sourceIntegrations ?? []).map((integration) => ({ - ...integration, - credentialId: '', - })); + const draftIntegrations = (sourceIntegrations ?? []).map((integration) => + integration.type === 'n8n_chat' ? integration : { ...integration, credentialId: '' }, + ); return await createAgent(context, projectId, name, { schema: { ...rest, name, integrations: draftIntegrations }, tools: agent.tools, diff --git a/packages/frontend/editor-ui/src/features/agents/composables/useAgentBuilderTelemetry.ts b/packages/frontend/editor-ui/src/features/agents/composables/useAgentBuilderTelemetry.ts index 9170e6b3280..c8e5ebbb552 100644 --- a/packages/frontend/editor-ui/src/features/agents/composables/useAgentBuilderTelemetry.ts +++ b/packages/frontend/editor-ui/src/features/agents/composables/useAgentBuilderTelemetry.ts @@ -52,7 +52,9 @@ function integrationStatusEntriesFromConfig( type: integration.type, credentialId: integration.credentialId, ...('settings' in integration ? { settings: integration.settings } : {}), - ...(integration.approval ? { approval: integration.approval } : {}), + ...('approval' in integration && integration.approval + ? { approval: integration.approval } + : {}), status: isPublished ? 'starting' : 'configured', }); } diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 64d2198c206..1c3c9ceae13 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -3476,6 +3476,8 @@ export interface RelatedAgentRun { * resume on the runtime they started on. */ previewChat?: boolean; + /** The published n8n Chat channel owns this run. */ + publishedN8nChat?: boolean; /** * The interactive n8n user, when there is one. The preview chat resumes the draft * agent version, which gates node and workflow tools by this user's access.