Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/__tests__/add-commands-filename.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe("Add Commands - Name-based Filenames", () => {
const agentConfig = getTemplateByName(agentName, "default");

// Call the API
const agentId = await createAgentApi({} as never, agentName, {}, {}, []);
const agentId = await createAgentApi({} as never, agentName, {}, {}, undefined, []);

// Generate config path using name (not ID)
const configPath = await generateUniqueFilename(
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/casing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("Key casing normalization", () => {
"Name",
conversation_config,
platform_settings,
undefined,
["prod"]
);

Expand Down Expand Up @@ -89,6 +90,7 @@ describe("Key casing normalization", () => {
"Name",
conversation_config,
undefined,
undefined,
["prod"]
);

Expand Down
335 changes: 335 additions & 0 deletions src/__tests__/workflow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
import { createAgentApi, updateAgentApi, getAgentApi } from "../shared/elevenlabs-api";
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

describe("Workflow support in agents", () => {
function makeMockClient(includeWorkflow: boolean = false) {
const mockWorkflow = includeWorkflow ? {
nodes: {
"start_node": {
type: "start",
position: { x: 0, y: 0 }
},
"agent_node": {
type: "agent",
position: { x: 100, y: 100 }
},
"end_node": {
type: "end",
position: { x: 200, y: 200 }
}
},
edges: {
"edge_1": {
from: "start_node",
to: "agent_node"
},
"edge_2": {
from: "agent_node",
to: "end_node"
}
}
} : undefined;

const create = jest.fn().mockResolvedValue({ agentId: "agent_workflow_123" });
const update = jest.fn().mockResolvedValue({ agentId: "agent_workflow_123" });
const get = jest.fn().mockResolvedValue({
agentId: "agent_workflow_123",
name: "Test Agent with Workflow",
conversationConfig: {
conversation: {
clientEvents: ["audio"],
},
agent: {
prompt: {
prompt: "Hello",
temperature: 0.5,
},
},
},
platformSettings: {
widget: { textInputEnabled: true },
},
workflow: mockWorkflow,
tags: ["workflow-test"],
});

return {
conversationalAi: {
agents: { create, update, get },
},
} as unknown as ElevenLabsClient;
}

describe("createAgentApi", () => {
it("should send workflow when provided", async () => {
const client = makeMockClient();
const conversation_config = {
conversation: {
client_events: ["audio"],
},
agent: { prompt: { prompt: "hi", temperature: 0 } },
} as unknown as Record<string, unknown>;

const workflow = {
nodes: {
"start": { type: "start", position: { x: 0, y: 0 } },
"end": { type: "end", position: { x: 100, y: 100 } }
},
edges: {
"edge_1": { from: "start", to: "end" }
}
};

await createAgentApi(
client,
"Agent with Workflow",
conversation_config,
undefined,
workflow,
["workflow"]
);

expect(client.conversationalAi.agents.create).toHaveBeenCalledTimes(1);
const payload = (client.conversationalAi.agents.create as jest.Mock).mock.calls[0][0];

expect(payload).toEqual(
expect.objectContaining({
name: "Agent with Workflow",
workflow: expect.objectContaining({
nodes: expect.objectContaining({
start: expect.any(Object),
end: expect.any(Object),
}),
edges: expect.objectContaining({
edge_1: expect.any(Object),
}),
}),
tags: ["workflow"],
})
);
});

it("should handle undefined workflow gracefully", async () => {
const client = makeMockClient();
const conversation_config = {
conversation: {
client_events: ["audio"],
},
agent: { prompt: { prompt: "hi", temperature: 0 } },
} as unknown as Record<string, unknown>;

await createAgentApi(
client,
"Agent without Workflow",
conversation_config,
undefined,
undefined,
[]
);

expect(client.conversationalAi.agents.create).toHaveBeenCalledTimes(1);
const payload = (client.conversationalAi.agents.create as jest.Mock).mock.calls[0][0];

expect(payload).toEqual(
expect.objectContaining({
name: "Agent without Workflow",
workflow: undefined,
})
);
});
});

describe("updateAgentApi", () => {
it("should send workflow when updating an agent", async () => {
const client = makeMockClient();
const conversation_config = {
conversation: {
client_events: ["audio"],
},
} as unknown as Record<string, unknown>;

const workflow = {
nodes: {
"updated_start": { type: "start", position: { x: 10, y: 10 } },
"updated_end": { type: "end", position: { x: 110, y: 110 } }
},
edges: {
"updated_edge": { from: "updated_start", to: "updated_end" }
}
};

await updateAgentApi(
client,
"agent_workflow_123",
"Updated Agent",
conversation_config,
undefined,
workflow,
["updated"]
);

expect(client.conversationalAi.agents.update).toHaveBeenCalledTimes(1);
const [agentId, payload] = (
client.conversationalAi.agents.update as jest.Mock
).mock.calls[0];

expect(agentId).toBe("agent_workflow_123");
expect(payload).toEqual(
expect.objectContaining({
name: "Updated Agent",
workflow: expect.objectContaining({
nodes: expect.objectContaining({
updated_start: expect.any(Object),
updated_end: expect.any(Object),
}),
}),
tags: ["updated"],
})
);
});

it("should allow clearing workflow by passing undefined", async () => {
const client = makeMockClient();
const conversation_config = {
conversation: {
client_events: ["audio"],
},
} as unknown as Record<string, unknown>;

await updateAgentApi(
client,
"agent_workflow_123",
"Agent Workflow Cleared",
conversation_config,
undefined,
undefined,
[]
);

expect(client.conversationalAi.agents.update).toHaveBeenCalledTimes(1);
const [, payload] = (
client.conversationalAi.agents.update as jest.Mock
).mock.calls[0];

expect(payload).toEqual(
expect.objectContaining({
workflow: undefined,
})
);
});
});

describe("getAgentApi", () => {
it("should return workflow when present in API response", async () => {
const client = makeMockClient(true);
const response = await getAgentApi(client, "agent_workflow_123");

expect(client.conversationalAi.agents.get).toHaveBeenCalledWith(
"agent_workflow_123"
);

expect(response).toEqual(
expect.objectContaining({
agent_id: "agent_workflow_123",
workflow: expect.objectContaining({
nodes: expect.any(Object),
edges: expect.any(Object),
}),
})
);

// Verify workflow structure
const responseTyped = response as { workflow: { nodes: Record<string, unknown>; edges: Record<string, unknown> } };
expect(responseTyped.workflow.nodes).toHaveProperty("start_node");
expect(responseTyped.workflow.nodes).toHaveProperty("agent_node");
expect(responseTyped.workflow.nodes).toHaveProperty("end_node");
expect(responseTyped.workflow.edges).toHaveProperty("edge_1");
expect(responseTyped.workflow.edges).toHaveProperty("edge_2");
});

it("should handle agents without workflow", async () => {
const client = makeMockClient(false);
const response = await getAgentApi(client, "agent_workflow_123");

expect(client.conversationalAi.agents.get).toHaveBeenCalledWith(
"agent_workflow_123"
);

expect(response).toEqual(
expect.objectContaining({
agent_id: "agent_workflow_123",
workflow: undefined,
})
);
});
});

describe("Workflow persistence in pull/push flow", () => {
it("should preserve complex workflow structures", async () => {
const client = makeMockClient();

// Complex workflow with multiple node types
const complexWorkflow = {
nodes: {
"start_1": {
type: "start",
position: { x: 0, y: 0 },
config: { initial_message: "Welcome" }
},
"agent_1": {
type: "override_agent",
position: { x: 100, y: 50 },
agent_id: "some_agent_id"
},
"tool_1": {
type: "tool",
position: { x: 200, y: 100 },
tool_id: "tool_123"
},
"end_1": {
type: "end",
position: { x: 300, y: 150 }
}
},
edges: {
"edge_start_to_agent": {
from: "start_1",
to: "agent_1",
condition: { type: "unconditional" }
},
"edge_agent_to_tool": {
from: "agent_1",
to: "tool_1",
condition: { type: "llm", description: "When user asks for help" }
},
"edge_tool_to_end": {
from: "tool_1",
to: "end_1",
condition: { type: "result", expected: "success" }
}
}
};

await createAgentApi(
client,
"Complex Workflow Agent",
{ agent: { prompt: { prompt: "test", temperature: 0 } } } as unknown as Record<string, unknown>,
undefined,
complexWorkflow,
["complex"]
);

const payload = (client.conversationalAi.agents.create as jest.Mock).mock.calls[0][0];

// Verify complex workflow is preserved
expect(payload.workflow).toEqual(complexWorkflow);
expect(payload.workflow.nodes).toHaveProperty("start_1");
expect(payload.workflow.nodes).toHaveProperty("agent_1");
expect(payload.workflow.nodes).toHaveProperty("tool_1");
expect(payload.workflow.nodes).toHaveProperty("end_1");
expect(payload.workflow.edges).toHaveProperty("edge_start_to_agent");
expect(payload.workflow.edges).toHaveProperty("edge_agent_to_tool");
expect(payload.workflow.edges).toHaveProperty("edge_tool_to_end");
});
});
});
2 changes: 2 additions & 0 deletions src/agents/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function createAddCommand(): Command {
// Extract config components
const conversationConfig = agentConfig.conversation_config || {};
const platformSettings = agentConfig.platform_settings;
const workflow = agentConfig.workflow;
const tags = agentConfig.tags || [];

// Create new agent
Expand All @@ -89,6 +90,7 @@ export function createAddCommand(): Command {
name,
conversationConfig,
platformSettings,
workflow,
tags
);

Expand Down
Loading
Loading