@swarmclawai/swarmclaw 1.3.6 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/package.json +9 -3
- package/src/.env.local +4 -0
- package/src/app/api/.well-known/agent-card/route.ts +46 -0
- package/src/app/api/a2a/route.ts +56 -0
- package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
- package/src/app/api/chats/[id]/deploy/route.ts +2 -2
- package/src/app/api/openclaw/sync/route.ts +1 -1
- package/src/app/api/swarmfeed/channels/route.ts +14 -0
- package/src/app/api/swarmfeed/posts/route.ts +60 -0
- package/src/app/api/swarmfeed/route.ts +37 -0
- package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
- package/src/app/protocols/page.tsx +16 -7
- package/src/app/swarmfeed/page.tsx +7 -0
- package/src/cli/index.js +19 -0
- package/src/cli/spec.js +8 -0
- package/src/components/agents/agent-avatar.tsx +2 -5
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/auth/access-key-gate.tsx +25 -0
- package/src/components/layout/sidebar-rail.tsx +52 -0
- package/src/components/protocols/builder/edge-editor.tsx +43 -0
- package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
- package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
- package/src/components/protocols/builder/edge-types/index.ts +3 -0
- package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
- package/src/components/protocols/builder/node-inspector.tsx +227 -0
- package/src/components/protocols/builder/node-palette.tsx +97 -0
- package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
- package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
- package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
- package/src/components/protocols/builder/node-types/index.ts +9 -0
- package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
- package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
- package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
- package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
- package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
- package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
- package/src/components/protocols/builder/run-overlay.tsx +29 -0
- package/src/components/protocols/builder/template-gallery.tsx +53 -0
- package/src/components/protocols/builder/validation-panel.tsx +57 -0
- package/src/components/skills/skills-workspace.tsx +1 -9
- package/src/features/protocols/builder/hooks/index.ts +2 -0
- package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
- package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
- package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
- package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
- package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
- package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
- package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
- package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
- package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
- package/src/features/swarmfeed/compose-post.tsx +139 -0
- package/src/features/swarmfeed/feed-page.tsx +136 -0
- package/src/features/swarmfeed/post-card.tsx +114 -0
- package/src/features/swarmfeed/queries.ts +28 -0
- package/src/lib/a2a/agent-card.ts +61 -0
- package/src/lib/a2a/auth.ts +54 -0
- package/src/lib/a2a/client.ts +133 -0
- package/src/lib/a2a/discovery.ts +116 -0
- package/src/lib/a2a/handlers.ts +176 -0
- package/src/lib/a2a/json-rpc-router.ts +38 -0
- package/src/lib/a2a/types.ts +95 -0
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/providers/anthropic.ts +111 -107
- package/src/lib/providers/openai.ts +146 -142
- package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
- package/src/lib/server/agents/main-agent-loop.ts +377 -41
- package/src/lib/server/chat-execution/chat-execution.ts +12 -7
- package/src/lib/server/extensions.ts +11 -0
- package/src/lib/server/openclaw/sync.ts +4 -4
- package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
- package/src/lib/server/protocols/protocol-normalization.ts +1 -0
- package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
- package/src/lib/server/protocols/protocol-types.ts +1 -0
- package/src/lib/server/session-tools/delegate.ts +151 -77
- package/src/lib/server/storage-auth.ts +10 -2
- package/src/lib/server/storage-normalization.ts +11 -0
- package/src/lib/server/storage.ts +100 -0
- package/src/lib/server/working-state/service.test.ts +2 -3
- package/src/lib/server/working-state/service.ts +37 -6
- package/src/lib/swarmfeed-client.ts +157 -0
- package/src/lib/validation/schemas.ts +1 -1
- package/src/stores/slices/data-slice.ts +3 -0
- package/src/stores/use-approval-store.ts +4 -1
- package/src/types/agent.ts +31 -1
- package/src/types/index.ts +1 -0
- package/src/types/protocol.ts +19 -0
- package/src/types/session.ts +1 -1
- package/src/types/swarmfeed.ts +30 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { devtools } from 'zustand/middleware'
|
|
3
|
+
import type { Node, Edge } from '@xyflow/react'
|
|
4
|
+
import type { ProtocolTemplate, ProtocolStepKind, ProtocolBranchCase, ProtocolRepeatConfig, ProtocolParallelConfig, ProtocolJoinConfig, ProtocolForEachConfig, ProtocolSubflowConfig, ProtocolSwarmConfig, ProtocolRunStepStatus } from '@/types'
|
|
5
|
+
|
|
6
|
+
export interface BuilderNodeData extends Record<string, unknown> {
|
|
7
|
+
label: string
|
|
8
|
+
kind: ProtocolStepKind
|
|
9
|
+
instructions?: string | null
|
|
10
|
+
turnLimit?: number | null
|
|
11
|
+
completionCriteria?: string | null
|
|
12
|
+
taskConfig?: { agentId?: string; title: string; description: string } | null
|
|
13
|
+
delegationConfig?: { agentId: string; message: string } | null
|
|
14
|
+
repeat?: ProtocolRepeatConfig | null
|
|
15
|
+
parallel?: ProtocolParallelConfig | null
|
|
16
|
+
join?: ProtocolJoinConfig | null
|
|
17
|
+
forEach?: ProtocolForEachConfig | null
|
|
18
|
+
subflow?: ProtocolSubflowConfig | null
|
|
19
|
+
swarm?: ProtocolSwarmConfig | null
|
|
20
|
+
branchCases?: ProtocolBranchCase[]
|
|
21
|
+
defaultNextStepId?: string | null
|
|
22
|
+
outputKey?: string | null
|
|
23
|
+
dependsOnStepIds?: string[]
|
|
24
|
+
runtimeStatus?: ProtocolRunStepStatus | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BuilderEdgeData extends Record<string, unknown> {
|
|
28
|
+
edgeType: 'default' | 'branch' | 'loop'
|
|
29
|
+
branchCaseId?: string | null
|
|
30
|
+
label?: string | null
|
|
31
|
+
isLoopback?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ValidationError {
|
|
35
|
+
nodeId?: string
|
|
36
|
+
edgeId?: string
|
|
37
|
+
message: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ValidationWarning {
|
|
41
|
+
nodeId?: string
|
|
42
|
+
message: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BuilderNode = Node<BuilderNodeData>
|
|
46
|
+
export type BuilderEdge = Edge<BuilderEdgeData>
|
|
47
|
+
|
|
48
|
+
interface UndoSnapshot {
|
|
49
|
+
nodes: BuilderNode[]
|
|
50
|
+
edges: BuilderEdge[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const MAX_UNDO_HISTORY = 50
|
|
54
|
+
|
|
55
|
+
export interface ProtocolBuilderState {
|
|
56
|
+
nodes: BuilderNode[]
|
|
57
|
+
edges: BuilderEdge[]
|
|
58
|
+
selectedNodeId: string | null
|
|
59
|
+
selectedEdgeId: string | null
|
|
60
|
+
isPaletteOpen: boolean
|
|
61
|
+
isInspectorOpen: boolean
|
|
62
|
+
currentTemplate: ProtocolTemplate | null
|
|
63
|
+
isDirty: boolean
|
|
64
|
+
validationErrors: ValidationError[]
|
|
65
|
+
validationWarnings: ValidationWarning[]
|
|
66
|
+
undoStack: UndoSnapshot[]
|
|
67
|
+
redoStack: UndoSnapshot[]
|
|
68
|
+
activeRunId: string | null
|
|
69
|
+
|
|
70
|
+
setNodes: (nodes: BuilderNode[]) => void
|
|
71
|
+
setEdges: (edges: BuilderEdge[]) => void
|
|
72
|
+
selectNode: (nodeId: string | null) => void
|
|
73
|
+
selectEdge: (edgeId: string | null) => void
|
|
74
|
+
updateNodeData: (nodeId: string, data: Partial<BuilderNodeData>) => void
|
|
75
|
+
updateEdgeData: (edgeId: string, data: Partial<BuilderEdgeData>) => void
|
|
76
|
+
addNode: (node: BuilderNode) => void
|
|
77
|
+
deleteNode: (nodeId: string) => void
|
|
78
|
+
addEdge: (edge: BuilderEdge) => void
|
|
79
|
+
deleteEdge: (edgeId: string) => void
|
|
80
|
+
loadTemplate: (template: ProtocolTemplate, nodes: BuilderNode[], edges: BuilderEdge[]) => void
|
|
81
|
+
setValidation: (errors: ValidationError[], warnings: ValidationWarning[]) => void
|
|
82
|
+
setActiveRun: (runId: string | null) => void
|
|
83
|
+
setDirty: (dirty: boolean) => void
|
|
84
|
+
pushUndo: () => void
|
|
85
|
+
undo: () => void
|
|
86
|
+
redo: () => void
|
|
87
|
+
setPaletteOpen: (open: boolean) => void
|
|
88
|
+
setInspectorOpen: (open: boolean) => void
|
|
89
|
+
reset: () => void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const initialState = {
|
|
93
|
+
nodes: [] as BuilderNode[],
|
|
94
|
+
edges: [] as BuilderEdge[],
|
|
95
|
+
selectedNodeId: null,
|
|
96
|
+
selectedEdgeId: null,
|
|
97
|
+
isPaletteOpen: true,
|
|
98
|
+
isInspectorOpen: true,
|
|
99
|
+
currentTemplate: null,
|
|
100
|
+
isDirty: false,
|
|
101
|
+
validationErrors: [] as ValidationError[],
|
|
102
|
+
validationWarnings: [] as ValidationWarning[],
|
|
103
|
+
undoStack: [] as UndoSnapshot[],
|
|
104
|
+
redoStack: [] as UndoSnapshot[],
|
|
105
|
+
activeRunId: null,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const useProtocolBuilderStore = create<ProtocolBuilderState>()(
|
|
109
|
+
devtools(
|
|
110
|
+
(set, get) => ({
|
|
111
|
+
...initialState,
|
|
112
|
+
|
|
113
|
+
setNodes: (nodes) => set({ nodes, isDirty: true }),
|
|
114
|
+
setEdges: (edges) => set({ edges, isDirty: true }),
|
|
115
|
+
|
|
116
|
+
selectNode: (nodeId) =>
|
|
117
|
+
set({ selectedNodeId: nodeId, selectedEdgeId: null }),
|
|
118
|
+
|
|
119
|
+
selectEdge: (edgeId) =>
|
|
120
|
+
set({ selectedEdgeId: edgeId, selectedNodeId: null }),
|
|
121
|
+
|
|
122
|
+
updateNodeData: (nodeId, data) => {
|
|
123
|
+
const { nodes } = get()
|
|
124
|
+
set({
|
|
125
|
+
nodes: nodes.map((n) =>
|
|
126
|
+
n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n,
|
|
127
|
+
),
|
|
128
|
+
isDirty: true,
|
|
129
|
+
})
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
updateEdgeData: (edgeId, data) => {
|
|
133
|
+
const { edges } = get()
|
|
134
|
+
set({
|
|
135
|
+
edges: edges.map((e) =>
|
|
136
|
+
e.id === edgeId ? { ...e, data: { ...e.data, ...data } as BuilderEdgeData } : e,
|
|
137
|
+
),
|
|
138
|
+
isDirty: true,
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
addNode: (node) => {
|
|
143
|
+
const { nodes } = get()
|
|
144
|
+
set({ nodes: [...nodes, node], isDirty: true })
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
deleteNode: (nodeId) => {
|
|
148
|
+
const { nodes, edges } = get()
|
|
149
|
+
set({
|
|
150
|
+
nodes: nodes.filter((n) => n.id !== nodeId),
|
|
151
|
+
edges: edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
|
152
|
+
selectedNodeId: null,
|
|
153
|
+
isDirty: true,
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
addEdge: (edge) => {
|
|
158
|
+
const { edges } = get()
|
|
159
|
+
set({ edges: [...edges, edge], isDirty: true })
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
deleteEdge: (edgeId) => {
|
|
163
|
+
const { edges } = get()
|
|
164
|
+
set({
|
|
165
|
+
edges: edges.filter((e) => e.id !== edgeId),
|
|
166
|
+
selectedEdgeId: null,
|
|
167
|
+
isDirty: true,
|
|
168
|
+
})
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
loadTemplate: (template, nodes, edges) =>
|
|
172
|
+
set({
|
|
173
|
+
currentTemplate: template,
|
|
174
|
+
nodes,
|
|
175
|
+
edges,
|
|
176
|
+
isDirty: false,
|
|
177
|
+
validationErrors: [],
|
|
178
|
+
validationWarnings: [],
|
|
179
|
+
undoStack: [],
|
|
180
|
+
redoStack: [],
|
|
181
|
+
selectedNodeId: null,
|
|
182
|
+
selectedEdgeId: null,
|
|
183
|
+
}),
|
|
184
|
+
|
|
185
|
+
setValidation: (errors, warnings) =>
|
|
186
|
+
set({ validationErrors: errors, validationWarnings: warnings }),
|
|
187
|
+
|
|
188
|
+
setActiveRun: (runId) => set({ activeRunId: runId }),
|
|
189
|
+
setDirty: (dirty) => set({ isDirty: dirty }),
|
|
190
|
+
|
|
191
|
+
pushUndo: () => {
|
|
192
|
+
const { nodes, edges, undoStack } = get()
|
|
193
|
+
const snapshot: UndoSnapshot = { nodes: [...nodes], edges: [...edges] }
|
|
194
|
+
const trimmed = undoStack.length >= MAX_UNDO_HISTORY
|
|
195
|
+
? undoStack.slice(1)
|
|
196
|
+
: undoStack
|
|
197
|
+
set({ undoStack: [...trimmed, snapshot], redoStack: [] })
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
undo: () => {
|
|
201
|
+
const { undoStack, nodes, edges } = get()
|
|
202
|
+
if (undoStack.length === 0) return
|
|
203
|
+
const prev = undoStack[undoStack.length - 1]
|
|
204
|
+
set({
|
|
205
|
+
nodes: prev.nodes,
|
|
206
|
+
edges: prev.edges,
|
|
207
|
+
undoStack: undoStack.slice(0, -1),
|
|
208
|
+
redoStack: [{ nodes, edges }, ...get().redoStack],
|
|
209
|
+
isDirty: true,
|
|
210
|
+
})
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
redo: () => {
|
|
214
|
+
const { redoStack, nodes, edges } = get()
|
|
215
|
+
if (redoStack.length === 0) return
|
|
216
|
+
const next = redoStack[0]
|
|
217
|
+
set({
|
|
218
|
+
nodes: next.nodes,
|
|
219
|
+
edges: next.edges,
|
|
220
|
+
redoStack: redoStack.slice(1),
|
|
221
|
+
undoStack: [...get().undoStack, { nodes, edges }],
|
|
222
|
+
isDirty: true,
|
|
223
|
+
})
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
setPaletteOpen: (open) => set({ isPaletteOpen: open }),
|
|
227
|
+
setInspectorOpen: (open) => set({ isInspectorOpen: open }),
|
|
228
|
+
|
|
229
|
+
reset: () => set(initialState),
|
|
230
|
+
}),
|
|
231
|
+
{ name: 'protocol-builder' },
|
|
232
|
+
),
|
|
233
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import dagre from 'dagre'
|
|
2
|
+
import type { Node, Edge } from '@xyflow/react'
|
|
3
|
+
import type { BuilderNodeData, BuilderEdgeData } from '../protocol-builder-store'
|
|
4
|
+
|
|
5
|
+
const NODE_WIDTH = 180
|
|
6
|
+
const NODE_HEIGHT = 80
|
|
7
|
+
const RANK_SEP = 100
|
|
8
|
+
const NODE_SEP = 60
|
|
9
|
+
|
|
10
|
+
export function getNodeLayout(
|
|
11
|
+
nodes: Node<BuilderNodeData>[],
|
|
12
|
+
edges: Edge<BuilderEdgeData>[],
|
|
13
|
+
): Node<BuilderNodeData>[] {
|
|
14
|
+
if (nodes.length === 0) return nodes
|
|
15
|
+
|
|
16
|
+
const g = new dagre.graphlib.Graph()
|
|
17
|
+
g.setGraph({ rankdir: 'TB', ranksep: RANK_SEP, nodesep: NODE_SEP })
|
|
18
|
+
g.setDefaultEdgeLabel(() => ({}))
|
|
19
|
+
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const edge of edges) {
|
|
25
|
+
g.setEdge(edge.source, edge.target)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dagre.layout(g)
|
|
29
|
+
|
|
30
|
+
return nodes.map((node) => {
|
|
31
|
+
const pos = g.node(node.id)
|
|
32
|
+
if (!pos) return node
|
|
33
|
+
return {
|
|
34
|
+
...node,
|
|
35
|
+
position: {
|
|
36
|
+
x: pos.x - NODE_WIDTH / 2,
|
|
37
|
+
y: pos.y - NODE_HEIGHT / 2,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { ProtocolTemplate, ProtocolStepDefinition } from '@/types'
|
|
4
|
+
import { templateToNodes } from './template-to-nodes'
|
|
5
|
+
import { nodesToTemplate } from './nodes-to-template'
|
|
6
|
+
|
|
7
|
+
function makeTemplate(overrides: Partial<ProtocolTemplate> = {}): ProtocolTemplate {
|
|
8
|
+
return {
|
|
9
|
+
id: 'tpl-1',
|
|
10
|
+
name: 'Test Template',
|
|
11
|
+
description: 'A test template',
|
|
12
|
+
builtIn: false,
|
|
13
|
+
defaultPhases: [],
|
|
14
|
+
...overrides,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeStep(overrides: Partial<ProtocolStepDefinition> = {}): ProtocolStepDefinition {
|
|
19
|
+
return {
|
|
20
|
+
id: 'step-1',
|
|
21
|
+
kind: 'present',
|
|
22
|
+
label: 'Step One',
|
|
23
|
+
...overrides,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Round-trip: templateToNodes → nodesToTemplate ---
|
|
28
|
+
|
|
29
|
+
describe('nodesToTemplate — round-trip', () => {
|
|
30
|
+
it('round-trips a simple 3-step template: preserves step count and entryStepId', () => {
|
|
31
|
+
const steps: ProtocolStepDefinition[] = [
|
|
32
|
+
makeStep({ id: 'step-1', label: 'Step One', nextStepId: 'step-2' }),
|
|
33
|
+
makeStep({ id: 'step-2', label: 'Step Two', kind: 'decide', nextStepId: 'step-3' }),
|
|
34
|
+
makeStep({ id: 'step-3', label: 'Step Three', kind: 'summarize' }),
|
|
35
|
+
]
|
|
36
|
+
const template = makeTemplate({ steps, entryStepId: 'step-1' })
|
|
37
|
+
const { nodes, edges } = templateToNodes(template)
|
|
38
|
+
const result = nodesToTemplate(nodes, edges, template)
|
|
39
|
+
|
|
40
|
+
assert.strictEqual(result.steps!.length, 3)
|
|
41
|
+
assert.deepStrictEqual(result.steps!.map((s) => s.id), ['step-1', 'step-2', 'step-3'])
|
|
42
|
+
assert.strictEqual(result.entryStepId, 'step-1')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('preserves nextStepId from default edges', () => {
|
|
46
|
+
const steps: ProtocolStepDefinition[] = [
|
|
47
|
+
makeStep({ id: 'step-a', nextStepId: 'step-b' }),
|
|
48
|
+
makeStep({ id: 'step-b', nextStepId: 'step-c' }),
|
|
49
|
+
makeStep({ id: 'step-c' }),
|
|
50
|
+
]
|
|
51
|
+
const template = makeTemplate({ steps })
|
|
52
|
+
const { nodes, edges } = templateToNodes(template)
|
|
53
|
+
const result = nodesToTemplate(nodes, edges, template)
|
|
54
|
+
|
|
55
|
+
const stepA = result.steps!.find((s) => s.id === 'step-a')
|
|
56
|
+
const stepB = result.steps!.find((s) => s.id === 'step-b')
|
|
57
|
+
const stepC = result.steps!.find((s) => s.id === 'step-c')
|
|
58
|
+
|
|
59
|
+
assert.strictEqual(stepA?.nextStepId, 'step-b')
|
|
60
|
+
assert.strictEqual(stepB?.nextStepId, 'step-c')
|
|
61
|
+
assert.strictEqual(stepC?.nextStepId, undefined)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('preserves step data fields: instructions, turnLimit, completionCriteria, outputKey', () => {
|
|
65
|
+
const steps: ProtocolStepDefinition[] = [
|
|
66
|
+
makeStep({
|
|
67
|
+
id: 'step-rich',
|
|
68
|
+
kind: 'decide',
|
|
69
|
+
label: 'Rich Step',
|
|
70
|
+
instructions: 'Evaluate carefully',
|
|
71
|
+
turnLimit: 5,
|
|
72
|
+
completionCriteria: 'Decision made',
|
|
73
|
+
outputKey: 'decision_result',
|
|
74
|
+
dependsOnStepIds: ['step-prev'],
|
|
75
|
+
}),
|
|
76
|
+
]
|
|
77
|
+
const template = makeTemplate({ steps })
|
|
78
|
+
const { nodes, edges } = templateToNodes(template)
|
|
79
|
+
const result = nodesToTemplate(nodes, edges, template)
|
|
80
|
+
|
|
81
|
+
const step = result.steps![0]
|
|
82
|
+
assert.strictEqual(step.id, 'step-rich')
|
|
83
|
+
assert.strictEqual(step.kind, 'decide')
|
|
84
|
+
assert.strictEqual(step.label, 'Rich Step')
|
|
85
|
+
assert.strictEqual(step.instructions, 'Evaluate carefully')
|
|
86
|
+
assert.strictEqual(step.turnLimit, 5)
|
|
87
|
+
assert.strictEqual(step.completionCriteria, 'Decision made')
|
|
88
|
+
assert.strictEqual(step.outputKey, 'decision_result')
|
|
89
|
+
assert.deepStrictEqual(step.dependsOnStepIds, ['step-prev'])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('preserves branchCases and defaultNextStepId', () => {
|
|
93
|
+
const steps: ProtocolStepDefinition[] = [
|
|
94
|
+
makeStep({
|
|
95
|
+
id: 'branch-step',
|
|
96
|
+
kind: 'branch',
|
|
97
|
+
label: 'Branch',
|
|
98
|
+
branchCases: [
|
|
99
|
+
{ id: 'case-yes', label: 'Yes', nextStepId: 'step-yes' },
|
|
100
|
+
{ id: 'case-no', label: 'No', nextStepId: 'step-no' },
|
|
101
|
+
],
|
|
102
|
+
defaultNextStepId: 'step-default',
|
|
103
|
+
}),
|
|
104
|
+
makeStep({ id: 'step-yes', label: 'Yes Path' }),
|
|
105
|
+
makeStep({ id: 'step-no', label: 'No Path' }),
|
|
106
|
+
makeStep({ id: 'step-default', label: 'Default Path' }),
|
|
107
|
+
]
|
|
108
|
+
const template = makeTemplate({ steps })
|
|
109
|
+
const { nodes, edges } = templateToNodes(template)
|
|
110
|
+
const result = nodesToTemplate(nodes, edges, template)
|
|
111
|
+
|
|
112
|
+
const branch = result.steps!.find((s) => s.id === 'branch-step')
|
|
113
|
+
assert.strictEqual(branch?.branchCases!.length, 2)
|
|
114
|
+
assert.strictEqual(branch?.branchCases![0].id, 'case-yes')
|
|
115
|
+
assert.strictEqual(branch?.branchCases![0].nextStepId, 'step-yes')
|
|
116
|
+
assert.strictEqual(branch?.branchCases![1].id, 'case-no')
|
|
117
|
+
assert.strictEqual(branch?.branchCases![1].nextStepId, 'step-no')
|
|
118
|
+
assert.strictEqual(branch?.defaultNextStepId, 'step-default')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('sets entryStepId from the first node', () => {
|
|
122
|
+
const steps: ProtocolStepDefinition[] = [
|
|
123
|
+
makeStep({ id: 'first-step', label: 'First' }),
|
|
124
|
+
makeStep({ id: 'second-step', label: 'Second' }),
|
|
125
|
+
]
|
|
126
|
+
const template = makeTemplate({ steps, entryStepId: 'first-step' })
|
|
127
|
+
const { nodes, edges } = templateToNodes(template)
|
|
128
|
+
const result = nodesToTemplate(nodes, edges, template)
|
|
129
|
+
|
|
130
|
+
assert.strictEqual(result.entryStepId, nodes[0].id)
|
|
131
|
+
assert.strictEqual(result.entryStepId, 'first-step')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// --- Direct nodesToTemplate tests (no round-trip) ---
|
|
136
|
+
|
|
137
|
+
describe('nodesToTemplate — direct construction', () => {
|
|
138
|
+
it('preserves all originalTemplate fields except steps and entryStepId', () => {
|
|
139
|
+
const template = makeTemplate({
|
|
140
|
+
id: 'custom-id',
|
|
141
|
+
name: 'Custom Name',
|
|
142
|
+
description: 'Custom description',
|
|
143
|
+
builtIn: true,
|
|
144
|
+
tags: ['tag-a', 'tag-b'],
|
|
145
|
+
singleAgentAllowed: true,
|
|
146
|
+
steps: [makeStep({ id: 'step-x' })],
|
|
147
|
+
})
|
|
148
|
+
const { nodes, edges } = templateToNodes(template)
|
|
149
|
+
const result = nodesToTemplate(nodes, edges, template)
|
|
150
|
+
|
|
151
|
+
assert.strictEqual(result.id, 'custom-id')
|
|
152
|
+
assert.strictEqual(result.name, 'Custom Name')
|
|
153
|
+
assert.strictEqual(result.description, 'Custom description')
|
|
154
|
+
assert.strictEqual(result.builtIn, true)
|
|
155
|
+
assert.deepStrictEqual(result.tags, ['tag-a', 'tag-b'])
|
|
156
|
+
assert.strictEqual(result.singleAgentAllowed, true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('handles an empty nodes array without throwing', () => {
|
|
160
|
+
const template = makeTemplate({ steps: [makeStep({ id: 'step-1' })], entryStepId: 'step-1' })
|
|
161
|
+
const result = nodesToTemplate([], [], template)
|
|
162
|
+
|
|
163
|
+
assert.strictEqual(result.steps!.length, 0)
|
|
164
|
+
// entryStepId falls back to originalTemplate.entryStepId when nodes is empty
|
|
165
|
+
assert.strictEqual(result.entryStepId, 'step-1')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('does not include nextStepId on steps with no outgoing default edge', () => {
|
|
169
|
+
const steps: ProtocolStepDefinition[] = [
|
|
170
|
+
makeStep({ id: 'lone-step', label: 'Lone Step' }),
|
|
171
|
+
]
|
|
172
|
+
const template = makeTemplate({ steps })
|
|
173
|
+
const { nodes, edges } = templateToNodes(template)
|
|
174
|
+
const result = nodesToTemplate(nodes, edges, template)
|
|
175
|
+
|
|
176
|
+
const step = result.steps![0]
|
|
177
|
+
assert.strictEqual(step.nextStepId, undefined)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Node, Edge } from '@xyflow/react'
|
|
2
|
+
import type { ProtocolTemplate, ProtocolStepDefinition } from '@/types'
|
|
3
|
+
import type { BuilderNodeData, BuilderEdgeData } from '../protocol-builder-store'
|
|
4
|
+
|
|
5
|
+
export function nodesToTemplate(
|
|
6
|
+
nodes: Node<BuilderNodeData>[],
|
|
7
|
+
edges: Edge<BuilderEdgeData>[],
|
|
8
|
+
originalTemplate: ProtocolTemplate,
|
|
9
|
+
): ProtocolTemplate {
|
|
10
|
+
const steps: ProtocolStepDefinition[] = nodes.map((node) => {
|
|
11
|
+
const step: ProtocolStepDefinition = {
|
|
12
|
+
id: node.id,
|
|
13
|
+
kind: node.data.kind,
|
|
14
|
+
label: node.data.label,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (node.data.instructions) step.instructions = node.data.instructions
|
|
18
|
+
if (node.data.turnLimit) step.turnLimit = node.data.turnLimit
|
|
19
|
+
if (node.data.completionCriteria) step.completionCriteria = node.data.completionCriteria
|
|
20
|
+
if (node.data.taskConfig) step.taskConfig = node.data.taskConfig
|
|
21
|
+
if (node.data.delegationConfig) step.delegationConfig = node.data.delegationConfig
|
|
22
|
+
if (node.data.repeat) step.repeat = node.data.repeat
|
|
23
|
+
if (node.data.parallel) step.parallel = node.data.parallel
|
|
24
|
+
if (node.data.join) step.join = node.data.join
|
|
25
|
+
if (node.data.forEach) step.forEach = node.data.forEach
|
|
26
|
+
if (node.data.subflow) step.subflow = node.data.subflow
|
|
27
|
+
if (node.data.swarm) step.swarm = node.data.swarm
|
|
28
|
+
if (node.data.branchCases?.length) step.branchCases = node.data.branchCases
|
|
29
|
+
if (node.data.defaultNextStepId) step.defaultNextStepId = node.data.defaultNextStepId
|
|
30
|
+
if (node.data.outputKey) step.outputKey = node.data.outputKey
|
|
31
|
+
if (node.data.dependsOnStepIds?.length) step.dependsOnStepIds = node.data.dependsOnStepIds
|
|
32
|
+
|
|
33
|
+
// Derive nextStepId from default edges
|
|
34
|
+
const defaultEdge = edges.find(
|
|
35
|
+
(e) => e.source === node.id && e.data?.edgeType === 'default',
|
|
36
|
+
)
|
|
37
|
+
if (defaultEdge) {
|
|
38
|
+
step.nextStepId = defaultEdge.target
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return step
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...originalTemplate,
|
|
46
|
+
steps,
|
|
47
|
+
entryStepId: nodes[0]?.id || originalTemplate.entryStepId,
|
|
48
|
+
}
|
|
49
|
+
}
|