From c96ce2f35fc638fc29c84c1c048cbfaea60b3d8b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 14:03:13 +0000 Subject: [PATCH] Remove flows-ai converters and the nanoid dependency (LOU-D35) Delete src/flows/converters.ts (+ test): it targeted a flows-ai engine that was never a dependency and FlowExecutor never used it. Replace every nanoid() call with a tiny newId() built on globalThis.crypto.randomUUID() (Node 22 and Cloudflare Workers both provide it) and drop nanoid from package.json and the lockfile. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR --- CHANGELOG.md | 4 + package-lock.json | 19 --- package.json | 1 - src/core/AgentBuilder.ts | 4 +- src/data/mocks.ts | 10 +- src/execution/AgentExecutor.ts | 4 +- src/execution/MemoryManager.ts | 4 +- src/execution/agentRun.ts | 4 +- src/execution/tracing.ts | 4 +- src/flows/FlowBuilder.ts | 4 +- src/flows/converters.test.ts | 265 ------------------------------- src/flows/converters.ts | 277 --------------------------------- src/flows/index.ts | 1 - src/session/sessionStream.ts | 4 +- src/types/flow.ts | 5 +- src/utils/errors.ts | 4 +- src/utils/id.test.ts | 16 ++ src/utils/id.ts | 11 ++ src/utils/json-path.ts | 4 +- 19 files changed, 56 insertions(+), 589 deletions(-) delete mode 100644 src/flows/converters.test.ts delete mode 100644 src/flows/converters.ts create mode 100644 src/utils/id.test.ts create mode 100644 src/utils/id.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 10debe3b5..ed5881ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `createAgent()` agents can pause for approval (LOU-D21): a `needsApproval` tool no longer fails the run with "requires approval but no approvalStore". The run pauses (`finishReason: 'awaiting-approval'`) in a per-agent `InMemoryApprovalStore` (or the new `approvalStore` option), and `agent.approvals.list()` / `agent.approvals.resolve({ id, approved, note? })` continue it, in its session if it paused in one. The `approve` option decides calls in code without pausing. - Tools own their contract (LOU-D22): `ToolDescriptor` gains optional `inputSchema` (zod) and `execute(args, ctx)`, which are now the canonical fields. `defineTool()` sets both and no longer calls `ai`'s `tool()`. Argument validation, the schema sent to the model, and tool execution read them first and fall back to `tool.parameters` / `tool.execute`. `ToolDescriptor.tool` (the `ai` v4 `Tool`) is now legacy: it is still built by `defineTool()` and still accepted on hand-written descriptors this release, but new code should set `inputSchema` and `execute`. +### Removed +- BREAKING: The flows-ai converters (LOU-D35) are gone: `convertToFlowDefinition()` and `convertFromFlowDefinition()` (`src/flows/converters.ts`) no longer ship from the package root or `./flows`. There is no replacement: the flows-ai engine they targeted was never a dependency of this package and `FlowExecutor` never used them. Run flows with `FlowExecutor` directly. +- `nanoid` is no longer a dependency (LOU-D35). Generated ids (agent, run, trace, memory ids) now come from `globalThis.crypto.randomUUID()` via an internal `newId()`, so they are UUIDs instead of 21-character nanoids. Nothing public depended on the old format. + ### Docs - README revamped into a short front page (LOU-D52); the details it dropped moved to `docs/` (new pages: tools, approvals, providers, cli, flows, guardrails, utilities). diff --git a/package-lock.json b/package-lock.json index f9f3e7271..d1cafa85d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.30.1", "dockerode": "^5.0.1", - "nanoid": "^5.0.7", "prompts": "^2.4.2", "undici": "^8.11.2", "yaml": "^2.9.1" @@ -8156,24 +8155,6 @@ "license": "MIT", "optional": true }, - "node_modules/nanoid": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", - "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index 82bbc9486..eaf7f3adf 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,6 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.30.1", "dockerode": "^5.0.1", - "nanoid": "^5.0.7", "prompts": "^2.4.2", "undici": "^8.11.2", "yaml": "^2.9.1" diff --git a/src/core/AgentBuilder.ts b/src/core/AgentBuilder.ts index 671adcd59..ff93e5f69 100644 --- a/src/core/AgentBuilder.ts +++ b/src/core/AgentBuilder.ts @@ -1,6 +1,6 @@ import { AgentConfig, AgentType, ToolConfiguration, AgentFlow } from '../types'; import { validateAgentConfig, validateAgentTools } from '../agent-types'; -import { nanoid } from 'nanoid'; +import { newId } from '../utils/id'; import type { DefinedTool } from '../tools/defineTool'; /** @@ -153,7 +153,7 @@ export class AgentBuilder { this.validate(); return { - id: this.config.id || nanoid(), + id: this.config.id || newId(), name: this.config.name!, ...(this.config.agentType !== undefined && { agentType: this.config.agentType }), locale: this.config.locale || 'en', diff --git a/src/data/mocks.ts b/src/data/mocks.ts index abfd8e5e8..85916ca33 100644 --- a/src/data/mocks.ts +++ b/src/data/mocks.ts @@ -13,7 +13,7 @@ import { PaginatedResult, } from './repositories'; import { Agent, Session, Result, Memory, Attachment } from './models'; -import { nanoid } from 'nanoid'; +import { newId } from '../utils/id'; /** * Base Mock Repository @@ -89,7 +89,7 @@ export class MockAgentRepository implements AgentRepository { protected generateId(): string { - return nanoid(); + return newId(); } async findByType(type: string): Promise { @@ -138,7 +138,7 @@ export class MockSessionRepository implements SessionRepository { protected generateId(): string { - return nanoid(); + return newId(); } async findByAgentId(agentId: string): Promise { @@ -183,7 +183,7 @@ export class MockResultRepository implements ResultRepository { protected generateId(): string { - return nanoid(); + return newId(); } async findBySessionId(sessionId: string): Promise { @@ -214,7 +214,7 @@ export class MockMemoryRepository implements MemoryRepository { protected generateId(): string { - return nanoid(); + return newId(); } /** diff --git a/src/execution/AgentExecutor.ts b/src/execution/AgentExecutor.ts index beee7edca..2d1cccd56 100644 --- a/src/execution/AgentExecutor.ts +++ b/src/execution/AgentExecutor.ts @@ -7,7 +7,7 @@ import type { Skill } from '../skills/defineSkill'; import { withSkills } from '../skills/withSkills'; import type { Subagents } from '../subagents/types'; import { assertMaxSubagentDepth, withSubagents } from '../subagents/withSubagents'; -import { nanoid } from 'nanoid'; +import { newId } from '../utils/id'; import { LLMProvider, Message, ToolCall, GenerateOptions, GenerateResult, ToolDefinition } from '../providers'; import { AgentConfig } from '../types'; import { ToolRegistry } from '../tools'; @@ -1022,7 +1022,7 @@ export class AgentExecutor { } const pending: PendingApproval = { - id: nanoid(), + id: newId(), toolCallId: toolCall.id, toolName: toolCall.function.name, args: toolResult.args || {}, diff --git a/src/execution/MemoryManager.ts b/src/execution/MemoryManager.ts index e0a874d9e..752f86a21 100644 --- a/src/execution/MemoryManager.ts +++ b/src/execution/MemoryManager.ts @@ -4,7 +4,7 @@ */ import { Memory, MemoryRepository } from '../data'; -import { nanoid } from 'nanoid'; +import { newId } from '../utils/id'; /** * Memory manager configuration @@ -124,7 +124,7 @@ export class MemoryManager { }; const memory = new Memory({ - id: nanoid(), + id: newId(), agentId, content, embedding: embedding ?? [], diff --git a/src/execution/agentRun.ts b/src/execution/agentRun.ts index 2581298c2..259d482d2 100644 --- a/src/execution/agentRun.ts +++ b/src/execution/agentRun.ts @@ -14,7 +14,7 @@ * buffered without loss until they are read. */ -import { nanoid } from 'nanoid'; +import { newId } from '../utils/id'; import type { GenerateOptions, GenerateResult, LLMProvider } from '../providers'; import type { ExecuteOptions, ExecutionEvent, ExecutionResult } from './AgentExecutor'; import type { PendingApproval } from './ApprovalGate'; @@ -138,7 +138,7 @@ function toJsonValue(value: unknown): unknown { type MeasuredStep = { finishReason: GenerateResult['finishReason']; usage: Usage; estimated: boolean; costUsd?: number }; class AgentRunImpl implements AgentRun { - readonly runId = nanoid(); + readonly runId = newId(); readonly result: Promise; private readonly controller = new AbortController(); private readonly queue: AgentEvent[] = []; diff --git a/src/execution/tracing.ts b/src/execution/tracing.ts index 06b5c05ad..5cdcd7f74 100644 --- a/src/execution/tracing.ts +++ b/src/execution/tracing.ts @@ -10,7 +10,7 @@ * for a console bridge and a real OTel bridge. */ -import { nanoid } from 'nanoid'; +import { newId } from '../utils/id'; import { ErrorAttr, LegacyAttr } from './semconv'; /** OpenTelemetry span kind, for the kinds this SDK emits. */ @@ -90,7 +90,7 @@ export async function withSpan( kind?: SpanKind ): Promise { const span: Span = { - id: nanoid(), + id: newId(), name, attributes, startTime: Date.now(), diff --git a/src/flows/FlowBuilder.ts b/src/flows/FlowBuilder.ts index 617b5b5aa..4c0b67764 100644 --- a/src/flows/FlowBuilder.ts +++ b/src/flows/FlowBuilder.ts @@ -1,4 +1,4 @@ -import { nanoid } from 'nanoid'; +import { newId } from '../utils/id'; import { EditorStep, AgentFlow, FlowInputVariable } from '../types'; /** Throw on the first input variable with a missing or duplicate name. */ @@ -107,7 +107,7 @@ export class FlowBuilder { this.validate(); return { - id: this.flow.id || nanoid(), + id: this.flow.id || newId(), code: this.flow.code!, name: this.flow.name!, description: this.flow.description, diff --git a/src/flows/converters.test.ts b/src/flows/converters.test.ts deleted file mode 100644 index 0b130e979..000000000 --- a/src/flows/converters.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { convertToFlowDefinition, convertFromFlowDefinition } from './converters'; -import { EditorStep } from '../types'; - -describe('Flow Converters', () => { - describe('convertToFlowDefinition', () => { - it('should convert simple step', () => { - const step: EditorStep = { - type: 'step', - agent: 'testAgent', - input: 'test input', - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('testAgent'); - expect(result.input).toBe('test input'); - expect(result.id).toBeDefined(); - }); - - it('should map executor-shaped nodes to the unknownAgent placeholder instead of throwing', () => { - const nodes: EditorStep[] = [ - { type: 'oneOf', options: [{ step: { type: 'end' } }] }, - { type: 'forEach', items: [1, 2] }, - { type: 'evaluator', expression: 'true' }, - { type: 'llmCall', prompt: 'hi' }, - ]; - - for (const node of nodes) { - expect(convertToFlowDefinition(node).agent).toBe('unknownAgent'); - } - }); - - it('should convert sequence', () => { - const step: EditorStep = { - type: 'sequence', - steps: [ - { type: 'step', agent: 'agent1', input: 'step 1' }, - { type: 'step', agent: 'agent2', input: 'step 2' }, - ], - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('sequenceAgent'); - expect(Array.isArray(result.input)).toBe(true); - expect(result.input).toHaveLength(2); - expect(result.input[0].agent).toBe('agent1'); - expect(result.input[1].agent).toBe('agent2'); - }); - - it('should convert parallel', () => { - const step: EditorStep = { - type: 'parallel', - steps: [ - { type: 'step', agent: 'agent1', input: 'step 1' }, - { type: 'step', agent: 'agent2', input: 'step 2' }, - ], - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('parallelAgent'); - expect(Array.isArray(result.input)).toBe(true); - expect(result.input).toHaveLength(2); - }); - - it('should convert oneOf', () => { - const step: EditorStep = { - type: 'oneOf', - branches: [ - { when: 'condition1', flow: { type: 'step', agent: 'agent1', input: 'flow 1' } }, - { when: 'condition2', flow: { type: 'step', agent: 'agent2', input: 'flow 2' } }, - ], - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('oneOfAgent'); - expect(result.conditions).toEqual(['condition1', 'condition2']); - expect(Array.isArray(result.input)).toBe(true); - expect(result.input).toHaveLength(2); - }); - - it('should convert forEach', () => { - const step: EditorStep = { - type: 'forEach', - item: 'user', - inputFlow: { type: 'step', agent: 'processUser', input: 'process @user' }, - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('forEachAgent'); - expect(result.item).toBe('user'); - expect(result.input.agent).toBe('processUser'); - }); - - it('should convert evaluator', () => { - const step: EditorStep = { - type: 'evaluator', - criteria: 'quality > 0.8', - max_iterations: 3, - subFlow: { type: 'step', agent: 'improve', input: 'improve quality' }, - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('optimizeAgent'); - expect(result.criteria).toBe('quality > 0.8'); - expect(result.max_iterations).toBe(3); - expect(result.input.agent).toBe('improve'); - }); - - it('should convert bestOfAll', () => { - const step: EditorStep = { - type: 'bestOfAll', - criteria: 'highest score', - steps: [ - { type: 'step', agent: 'approach1', input: 'method 1' }, - { type: 'step', agent: 'approach2', input: 'method 2' }, - ], - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('bestOfAllAgent'); - expect(result.criteria).toBe('highest score'); - expect(result.input).toHaveLength(2); - }); - - it('should convert tool', () => { - const step: EditorStep = { - type: 'tool', - toolName: 'httpTool', - toolOptions: { url: 'https://api.example.com' }, - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('toolAgent'); - const parsed = JSON.parse(result.input); - expect(parsed.toolName).toBe('httpTool'); - expect(parsed.toolOptions.url).toBe('https://api.example.com'); - }); - - it('should convert condition', () => { - const step: EditorStep = { - type: 'condition', - condition: 'x > 5', - trueFlow: { type: 'step', agent: 'ifTrue', input: 'true branch' }, - falseFlow: { type: 'step', agent: 'ifFalse', input: 'false branch' }, - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('oneOfAgent'); - expect(result.conditions).toEqual(['x > 5', '!(x > 5)']); - expect(result.input).toHaveLength(2); - }); - - it('should handle nested structures', () => { - const step: EditorStep = { - type: 'sequence', - steps: [ - { - type: 'parallel', - steps: [ - { type: 'step', agent: 'agent1', input: 'parallel 1' }, - { type: 'step', agent: 'agent2', input: 'parallel 2' }, - ], - }, - { type: 'step', agent: 'agent3', input: 'after parallel' }, - ], - }; - - const result = convertToFlowDefinition(step); - - expect(result.agent).toBe('sequenceAgent'); - expect(result.input[0].agent).toBe('parallelAgent'); - expect(result.input[1].agent).toBe('agent3'); - }); - }); - - describe('convertFromFlowDefinition', () => { - it('should convert back to step', () => { - const flowDef = { - id: 'test-id', - agent: 'testAgent', - input: 'test input', - }; - - const result = convertFromFlowDefinition(flowDef); - - expect(result.type).toBe('step'); - if (result.type === 'step') { - expect(result.agent).toBe('testAgent'); - expect(result.input).toBe('test input'); - } - }); - - it('should convert back to sequence', () => { - const flowDef = { - id: 'seq-id', - agent: 'sequenceAgent', - input: [ - { id: 'step1', agent: 'agent1', input: 'step 1' }, - { id: 'step2', agent: 'agent2', input: 'step 2' }, - ], - }; - - const result = convertFromFlowDefinition(flowDef); - - expect(result.type).toBe('sequence'); - if (result.type === 'sequence') { - expect(result.steps).toHaveLength(2); - } - }); - - it('should convert back to oneOf', () => { - const flowDef = { - id: 'oneof-id', - agent: 'oneOfAgent', - input: [ - { id: 'flow1', agent: 'agent1', input: 'flow 1' }, - { id: 'flow2', agent: 'agent2', input: 'flow 2' }, - ], - conditions: ['cond1', 'cond2'], - }; - - const result = convertFromFlowDefinition(flowDef); - - expect(result.type).toBe('oneOf'); - if (result.type === 'oneOf') { - expect(result.branches).toHaveLength(2); - expect(result.branches[0].when).toBe('cond1'); - } - }); - - it('should round-trip correctly', () => { - const original: EditorStep = { - type: 'sequence', - steps: [ - { type: 'step', agent: 'agent1', input: 'step 1' }, - { - type: 'parallel', - steps: [ - { type: 'step', agent: 'agent2', input: 'step 2' }, - { type: 'step', agent: 'agent3', input: 'step 3' }, - ], - }, - ], - }; - - const flowDef = convertToFlowDefinition(original); - const backToEditor = convertFromFlowDefinition(flowDef); - - expect(backToEditor.type).toBe('sequence'); - if (backToEditor.type === 'sequence') { - expect(backToEditor.steps).toHaveLength(2); - expect(backToEditor.steps[1].type).toBe('parallel'); - } - }); - }); -}); diff --git a/src/flows/converters.ts b/src/flows/converters.ts deleted file mode 100644 index e8e0174e3..000000000 --- a/src/flows/converters.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { nanoid } from 'nanoid'; -import { - EditorStep, - EditorShapeStep, - StepNode, - SequenceNode, - ParallelNode, - OneOfNode, - ForEachNode, - EvaluatorNode, - BestOfAllNode, - ToolNode, - UIComponentNode, - ConditionNode, - LoopNode, -} from '../types'; - -const convertChildren = (steps: EditorStep[]): any[] => - steps.map((child) => convertToFlowDefinition(child)); - -// ------------------------------------ -// EditorStep -> FlowDefinition, one converter per step type -// ------------------------------------ -type StepConverters = { [K in EditorShapeStep['type']]: (step: Extract) => any }; - -const STEP_CONVERTERS: StepConverters = { - // STEP - Basic agent execution - step: (step: StepNode) => ({ - id: nanoid(), - agent: step.agent, - input: step.input, - }), - - // SEQUENCE - Execute steps in order - sequence: (step: SequenceNode) => ({ - id: nanoid(), - agent: 'sequenceAgent', - input: convertChildren(step.steps), - }), - - // PARALLEL - Execute steps concurrently - parallel: (step: ParallelNode) => ({ - id: nanoid(), - agent: 'parallelAgent', - input: convertChildren(step.steps), - }), - - // ONE-OF - Conditional branching - oneOf: (step: OneOfNode) => { - const flows = step.branches.map((b) => convertToFlowDefinition(b.flow)); - const conditions = step.branches.map((b) => b.when); - return { - id: nanoid(), - agent: 'oneOfAgent', - input: flows, - conditions: conditions, - }; - }, - - // FOR-EACH - Iterate over items - forEach: (step: ForEachNode) => ({ - id: nanoid(), - agent: 'forEachAgent', - item: step.item, - input: convertToFlowDefinition(step.inputFlow), - }), - - // EVALUATOR - Self-improvement loop - evaluator: (step: EvaluatorNode) => ({ - id: nanoid(), - agent: 'optimizeAgent', - criteria: step.criteria, - max_iterations: step.max_iterations, - input: convertToFlowDefinition(step.subFlow), - }), - - // BEST-OF-ALL - Run multiple and pick best - bestOfAll: (step: BestOfAllNode) => ({ - id: nanoid(), - agent: 'bestOfAllAgent', - criteria: step.criteria, - input: convertChildren(step.steps), - }), - - // TOOL - Direct tool execution - tool: (step: ToolNode) => ({ - id: nanoid(), - agent: 'toolAgent', - input: JSON.stringify({ - toolName: step.toolName, - toolOptions: step.toolOptions, - }), - }), - - // UI-COMPONENT - UI rendering step - uiComponent: (step: UIComponentNode) => ({ - id: nanoid(), - agent: 'uiComponentAgent', - input: JSON.stringify({ - componentName: step.componentName, - componentProps: step.componentProps, - }), - }), - - // CONDITION - If-then-else - condition: (step: ConditionNode) => ({ - id: nanoid(), - agent: 'oneOfAgent', - input: [ - convertToFlowDefinition(step.trueFlow), - convertToFlowDefinition(step.falseFlow), - ], - conditions: [step.condition, `!(${step.condition})`], - }), - - // LOOP - While loop - loop: (step: LoopNode) => ({ - id: nanoid(), - agent: 'forEachAgent', - item: 'iteration', - input: convertToFlowDefinition(step.loopFlow), - maxIterations: step.maxIterations, - condition: step.condition, - }), -}; - -const STEP_CONVERTER_BY_TYPE: ReadonlyMap any> = new Map( - Object.entries(STEP_CONVERTERS) -); - -// Executor-side shapes of node types that also have an editor shape -// (`options` / `items` / `expression` instead of branches / inputFlow / subFlow); -// they have no flows-ai equivalent, so they convert to the unknownAgent placeholder. -const EXECUTOR_ONLY_FIELD: Readonly> = { - oneOf: 'options', - forEach: 'items', - evaluator: 'expression', -}; - -function isExecutorShaped(step: EditorStep): boolean { - const field = EXECUTOR_ONLY_FIELD[step.type]; - return field !== undefined && field in step; -} - -/** - * Convert EditorStep to flows-ai compatible FlowDefinition - * This recursively transforms the internal EditorStep structure - * to the format expected by the flows-ai execution engine - */ -export function convertToFlowDefinition(step: EditorStep): any { - const convert = isExecutorShaped(step) ? undefined : STEP_CONVERTER_BY_TYPE.get(step.type); - if (convert) { - return convert(step); - } - - return { - id: nanoid(), - agent: 'unknownAgent', - input: '', - }; -} - -const convertFromChildren = (input: any): EditorStep[] => - Array.isArray(input) ? input.map((child: any) => convertFromFlowDefinition(child)) : []; - -/** - * Tool and UI-component agents carry their payload as a JSON string in - * `input`. If it cannot be parsed, fall back to a plain step. - */ -function convertJsonPayloadAgent( - flowDef: any, - agent: string, - fromPayload: (payload: any) => EditorStep -): EditorStep { - try { - return fromPayload(JSON.parse(flowDef.input)); - } catch { - return { - type: 'step', - agent, - input: flowDef.input, - }; - } -} - -// ------------------------------------ -// FlowDefinition -> EditorStep, one converter per agent name -// ------------------------------------ -const AGENT_CONVERTERS: ReadonlyMap EditorStep> = new Map< - string, - (flowDef: any) => EditorStep ->([ - ['sequenceAgent', (flowDef) => ({ type: 'sequence', steps: convertFromChildren(flowDef.input) })], - - ['parallelAgent', (flowDef) => ({ type: 'parallel', steps: convertFromChildren(flowDef.input) })], - - [ - 'oneOfAgent', - (flowDef) => { - const conditions = flowDef.conditions || []; - const flows = Array.isArray(flowDef.input) ? flowDef.input : []; - return { - type: 'oneOf', - branches: flows.map((flow: any, i: number) => ({ - when: conditions[i] || '', - flow: convertFromFlowDefinition(flow), - })), - }; - }, - ], - - [ - 'forEachAgent', - (flowDef) => ({ - type: 'forEach', - item: flowDef.item || '', - inputFlow: convertFromFlowDefinition(flowDef.input), - }), - ], - - [ - 'optimizeAgent', - (flowDef) => ({ - type: 'evaluator', - criteria: flowDef.criteria || '', - max_iterations: flowDef.max_iterations, - subFlow: convertFromFlowDefinition(flowDef.input), - }), - ], - - [ - 'bestOfAllAgent', - (flowDef) => ({ - type: 'bestOfAll', - criteria: flowDef.criteria || '', - steps: convertFromChildren(flowDef.input), - }), - ], - - [ - 'toolAgent', - (flowDef) => - convertJsonPayloadAgent(flowDef, 'toolAgent', (parsed) => ({ - type: 'tool', - toolName: parsed.toolName || '', - toolOptions: parsed.toolOptions || {}, - })), - ], - - [ - 'uiComponentAgent', - (flowDef) => - convertJsonPayloadAgent(flowDef, 'uiComponentAgent', (parsed) => ({ - type: 'uiComponent', - componentName: parsed.componentName || '', - componentProps: parsed.componentProps || {}, - })), - ], -]); - -/** - * Convert flows-ai FlowDefinition back to EditorStep - * Useful for round-tripping and editing - */ -export function convertFromFlowDefinition(flowDef: any): EditorStep { - const agent = flowDef.agent; - const convert = AGENT_CONVERTERS.get(agent); - if (convert) { - return convert(flowDef); - } - - return { - type: 'step', - agent: agent || 'defaultAgent', - input: typeof flowDef.input === 'string' ? flowDef.input : JSON.stringify(flowDef.input), - }; -} diff --git a/src/flows/index.ts b/src/flows/index.ts index 84ee8343c..e3960a34a 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -1,5 +1,4 @@ export * from './FlowBuilder'; export * from './FlowExecutor'; -export * from './converters'; export * from './inputs'; export * from './validators'; diff --git a/src/session/sessionStream.ts b/src/session/sessionStream.ts index 51c410456..1331f978d 100644 --- a/src/session/sessionStream.ts +++ b/src/session/sessionStream.ts @@ -6,7 +6,7 @@ * `run.done` is delivered only after the turn has been persisted. */ -import { nanoid } from 'nanoid'; +import { newId } from '../utils/id'; import type { ExecutionResult } from '../execution/AgentExecutor'; import { AGENT_EVENT_SCHEMA_VERSION, type AgentEvent, type AgentEventPayload } from '../execution/agentEvents'; import type { AgentRun } from '../execution/agentRun'; @@ -18,7 +18,7 @@ import type { AgentRun } from '../execution/agentRun'; export type SessionTurn = (signal: AbortSignal, started: (run: AgentRun) => void) => Promise; class SessionRun implements AgentRun { - readonly runId = nanoid(); + readonly runId = newId(); readonly result: Promise; private readonly controller = new AbortController(); private readonly started: Promise; diff --git a/src/types/flow.ts b/src/types/flow.ts index bb42a099d..acdbbe907 100644 --- a/src/types/flow.ts +++ b/src/types/flow.ts @@ -110,8 +110,7 @@ export type FlowOutputMode = 'stream' | 'buffer'; /** * A flow node: every shape `FlowExecutor` can run, discriminated on `type`. * - * The first group are the editor-side shapes (also what the flows-ai - * converters understand); the second group are the executor-side shapes the + * The first group are the editor-side shapes; the second group are the executor-side shapes the * handler map in `FlowExecutor` reads. `oneOf`, `forEach` and `evaluator` * appear in both groups with different fields: `oneOf` has `branches` * (editor) or `options` (executor), and so on, so narrow on those fields too. @@ -135,7 +134,7 @@ export type FlowOutputMode = 'stream' | 'buffer'; */ export type EditorStep = EditorShapeStep | RuntimeStep; -/** Editor-side node shapes (what the flows-ai converters accept). */ +/** Editor-side node shapes (what visual editors produce). */ export type EditorShapeStep = | StepNode | SequenceNode diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 6c4adeaff..658840b94 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,5 +1,5 @@ import { ZodError, type ZodIssue } from 'zod'; -import { nanoid } from 'nanoid'; +import { newId } from './id'; type ErrorWithMessage = { message: string; @@ -53,7 +53,7 @@ export function formatZodError(err: unknown): { type: string; message: string; [ .join('\n\n'); errorChunk = { - flowNodeId: nanoid(), + flowNodeId: newId(), type: 'error', message: formattedIssues, }; diff --git a/src/utils/id.test.ts b/src/utils/id.test.ts new file mode 100644 index 000000000..3e4adc698 --- /dev/null +++ b/src/utils/id.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest'; +import { newId } from './id'; + +describe('newId', () => { + it('returns a UUID', () => { + expect(newId()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + }); + + it('joins a prefix with an underscore', () => { + expect(newId('run')).toMatch(/^run_[0-9a-f-]{36}$/); + }); + + it('does not repeat', () => { + expect(new Set(Array.from({ length: 1000 }, () => newId())).size).toBe(1000); + }); +}); diff --git a/src/utils/id.ts b/src/utils/id.ts new file mode 100644 index 000000000..f87aefe70 --- /dev/null +++ b/src/utils/id.ts @@ -0,0 +1,11 @@ +/** + * Generates a unique id. Uses the Web Crypto `randomUUID()` that Node 22+ and + * Cloudflare Workers both provide as a global, so it needs no dependency and + * no `node:*` import (the deploy runtime bundles for Workers). + * + * @param prefix Optional prefix, joined to the id with an underscore. + */ +export function newId(prefix?: string): string { + const id = globalThis.crypto.randomUUID(); + return prefix ? `${prefix}_${id}` : id; +} diff --git a/src/utils/json-path.ts b/src/utils/json-path.ts index 54e94a39f..6bed94965 100644 --- a/src/utils/json-path.ts +++ b/src/utils/json-path.ts @@ -1,4 +1,4 @@ -import { nanoid } from 'nanoid'; +import { newId } from './id'; /** * Recursively traverses the given object (and its children via "input" and "item") @@ -11,7 +11,7 @@ export function setRecursiveNames(obj: any, path: string[] = []): void { } if (!obj.id) { - obj.id = nanoid(); + obj.id = newId(); } // If this node has an 'agent', extend our path