feat(workflow-tools): add get_logic_function_source tool (#22835)
## What / why
The workflow agent tools let an AI **write** a CODE step's logic
function (`update_logic_function_source`) and **list** logic functions
(`list_logic_function_tools`), but there is no tool to **read** an
existing function's source.
That's fine for greenfield generation — the agent already has in context
whatever code it just wrote. But it's a real gap when editing a function
the agent did **not** author: to safely modify an existing CODE step it
has to see the current source first, and today the only ways to get it
are the frontend (`getLogicFunctionSourceCode` query) or the DB. So the
agent is forced to either guess or ask a human to paste the code.
This adds a small read tool that closes the loop, mirroring the existing
`update_logic_function_source` tool.
## How
- New `get_logic_function_source` tool that calls the existing
`LogicFunctionFromSourceService.getSourceCode({ id, workspaceId })` —
the same service method backing the `getLogicFunctionSourceCode` GraphQL
resolver the frontend already uses. No new service logic.
- Registered in `workflow-tool.workspace-service.ts` alongside
`update_logic_function_source` (the dependency
`logicFunctionFromSourceService` is already injected).
- Unit test covering the success and error paths, matching the `get-*`
tool test convention.
## Notes
- Read-only, additive; no schema or API changes.
- Naturally pairs with `update_logic_function_source`: read → edit →
write.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22835?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
+6
@@ -25,6 +25,7 @@ import { createDeactivateWorkflowVersionTool } from 'src/modules/workflow/workfl
|
||||
import { createDeleteWorkflowTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow.tool';
|
||||
import { createDeleteWorkflowVersionEdgeTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow-version-edge.tool';
|
||||
import { createDeleteWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow-version-step.tool';
|
||||
import { createGetLogicFunctionSourceTool } from 'src/modules/workflow/workflow-tools/tools/get-logic-function-source.tool';
|
||||
import { createGetWorkflowCurrentVersionTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool';
|
||||
import { createGetWorkflowRunTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-run.tool';
|
||||
import { createListLogicFunctionToolsTool } from 'src/modules/workflow/workflow-tools/tools/list-logic-function-tools.tool';
|
||||
@@ -147,6 +148,10 @@ export class WorkflowToolWorkspaceService {
|
||||
this.deps,
|
||||
contextWithPermissions,
|
||||
);
|
||||
const getLogicFunctionSource = createGetLogicFunctionSourceTool(
|
||||
this.deps,
|
||||
context,
|
||||
);
|
||||
const updateLogicFunctionSource = createUpdateLogicFunctionSourceTool(
|
||||
this.deps,
|
||||
context,
|
||||
@@ -176,6 +181,7 @@ export class WorkflowToolWorkspaceService {
|
||||
[deleteWorkflow.name]: deleteWorkflow,
|
||||
[getWorkflowRun.name]: getWorkflowRun,
|
||||
[listWorkflowRuns.name]: listWorkflowRuns,
|
||||
[getLogicFunctionSource.name]: getLogicFunctionSource,
|
||||
[updateLogicFunctionSource.name]: updateLogicFunctionSource,
|
||||
[listLogicFunctionTools.name]: listLogicFunctionTools,
|
||||
[updateAgent.name]: updateAgent,
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
type WorkflowToolContext,
|
||||
type WorkflowToolDependencies,
|
||||
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
|
||||
|
||||
import { createGetLogicFunctionSourceTool } from '../get-logic-function-source.tool';
|
||||
|
||||
const WORKSPACE_ID = '20202020-aaaa-4d02-bf25-6aeccf7ea419';
|
||||
const LOGIC_FUNCTION_ID = '20202020-bbbb-4d02-bf25-6aeccf7ea419';
|
||||
|
||||
const buildDeps = (getSourceCode: jest.Mock) =>
|
||||
({
|
||||
logicFunctionFromSourceService: { getSourceCode },
|
||||
}) as unknown as Pick<
|
||||
WorkflowToolDependencies,
|
||||
'logicFunctionFromSourceService'
|
||||
>;
|
||||
|
||||
const buildContext = () =>
|
||||
({ workspaceId: WORKSPACE_ID }) as unknown as WorkflowToolContext;
|
||||
|
||||
describe('get_logic_function_source tool', () => {
|
||||
it('returns the source code from the service', async () => {
|
||||
const source = 'export const main = async () => ({ ok: true });';
|
||||
const getSourceCode = jest.fn().mockResolvedValue(source);
|
||||
|
||||
const tool = createGetLogicFunctionSourceTool(
|
||||
buildDeps(getSourceCode),
|
||||
buildContext(),
|
||||
);
|
||||
|
||||
const result = await tool.execute({ logicFunctionId: LOGIC_FUNCTION_ID });
|
||||
|
||||
expect(getSourceCode).toHaveBeenCalledWith({
|
||||
id: LOGIC_FUNCTION_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
logicFunctionId: LOGIC_FUNCTION_ID,
|
||||
sourceHandlerCode: source,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a failure result when the service throws', async () => {
|
||||
const getSourceCode = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Logic function not found'));
|
||||
|
||||
const tool = createGetLogicFunctionSourceTool(
|
||||
buildDeps(getSourceCode),
|
||||
buildContext(),
|
||||
);
|
||||
|
||||
const result = await tool.execute({ logicFunctionId: LOGIC_FUNCTION_ID });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Logic function not found');
|
||||
});
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type WorkflowToolContext,
|
||||
type WorkflowToolDependencies,
|
||||
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
|
||||
|
||||
const getLogicFunctionSourceSchema = z.object({
|
||||
logicFunctionId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe(
|
||||
'The ID of the logic function to read (from the code step settings.input.logicFunctionId)',
|
||||
),
|
||||
});
|
||||
|
||||
export const createGetLogicFunctionSourceTool = (
|
||||
deps: Pick<WorkflowToolDependencies, 'logicFunctionFromSourceService'>,
|
||||
context: WorkflowToolContext,
|
||||
) => ({
|
||||
name: 'get_logic_function_source' as const,
|
||||
description: `Read the current TypeScript source code of a logic function used in a workflow CODE step.
|
||||
|
||||
Use this to inspect the code that runs when a CODE step executes — for example, before editing an existing function with update_logic_function_source, so the edit is based on the real current source rather than a guess.
|
||||
|
||||
To find the logicFunctionId, look at the code step's settings.input.logicFunctionId field.`,
|
||||
inputSchema: getLogicFunctionSourceSchema,
|
||||
execute: async (parameters: { logicFunctionId: string }) => {
|
||||
try {
|
||||
const { logicFunctionId } = parameters;
|
||||
const { workspaceId } = context;
|
||||
|
||||
const sourceHandlerCode =
|
||||
await deps.logicFunctionFromSourceService.getSourceCode({
|
||||
id: logicFunctionId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
logicFunctionId,
|
||||
sourceHandlerCode,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
message: `Failed to read logic function source: ${message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user