agentplane-core 0.0.1__tar.gz

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.
@@ -0,0 +1,34 @@
1
+ # internal working documents — not part of the public repo
2
+ CLAUDE.md
3
+ SPEC.md
4
+ .claude/
5
+
6
+ # python
7
+ __pycache__/
8
+ *.py[cod]
9
+ *.egg-info/
10
+ .venv/
11
+ dist/
12
+ build/
13
+
14
+ # tooling caches
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ # local databases and state
22
+ *.db
23
+ registry.db
24
+ runtime.db
25
+
26
+ # environments / secrets
27
+ .env
28
+ .env.*
29
+
30
+ # editors / OS
31
+ .vscode/
32
+ .idea/
33
+ .DS_Store
34
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentplane-core
3
+ Version: 0.0.1
4
+ Summary: agentplane shared contract: Definition schema, registry models, interfaces
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: a2a-sdk<2,>=1.1
8
+ Requires-Dist: pydantic<3,>=2.7
9
+ Description-Content-Type: text/markdown
10
+
11
+ # agentplane-core
12
+
13
+ Shared contract of the agentplane platform: the Flow **Definition** schema
14
+ (Pydantic v2), registry/validation models, and the `SecretsProvider` /
15
+ `VectorStore` / `SearchBackend` interfaces.
16
+
17
+ - Depends on `pydantic` and `a2a-sdk` only — no I/O, no HTTP, no DB code.
18
+ - `agentplane_core.validation.validate_structure()` runs every stateless
19
+ check (stable `E0xx` codes) and is the exact code the runtime uses.
20
+ - `python -m agentplane_core.schema_export` emits the public JSON Schema for
21
+ builders and AI-generated definitions.
@@ -0,0 +1,11 @@
1
+ # agentplane-core
2
+
3
+ Shared contract of the agentplane platform: the Flow **Definition** schema
4
+ (Pydantic v2), registry/validation models, and the `SecretsProvider` /
5
+ `VectorStore` / `SearchBackend` interfaces.
6
+
7
+ - Depends on `pydantic` and `a2a-sdk` only — no I/O, no HTTP, no DB code.
8
+ - `agentplane_core.validation.validate_structure()` runs every stateless
9
+ check (stable `E0xx` codes) and is the exact code the runtime uses.
10
+ - `python -m agentplane_core.schema_export` emits the public JSON Schema for
11
+ builders and AI-generated definitions.
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "agentplane-core"
3
+ version = "0.0.1"
4
+ description = "agentplane shared contract: Definition schema, registry models, interfaces"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ # core depends on pydantic and a2a-sdk — nothing else (CLAUDE.md dependency policy)
10
+ "pydantic>=2.7,<3",
11
+ "a2a-sdk>=1.1,<2",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/agentplane_core"]
@@ -0,0 +1,158 @@
1
+ """agentplane-core: shared contract — Definition schema, registry models, interfaces."""
2
+
3
+ from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
4
+
5
+ from agentplane_core.card_json import agent_card_from_dict, agent_card_to_json_dict
6
+ from agentplane_core.definition import (
7
+ DEPRECATED_NODE_VERSIONS,
8
+ NODE_CATALOG,
9
+ SCHEMA_VERSION,
10
+ Edge,
11
+ EndNode,
12
+ EndNodeConfig,
13
+ ExposeConfig,
14
+ FlowDefinition,
15
+ Layout,
16
+ LayoutPosition,
17
+ LlmCallNode,
18
+ LlmCallNodeConfig,
19
+ McpToolNode,
20
+ McpToolNodeConfig,
21
+ Node,
22
+ RetrievalNode,
23
+ RetrievalNodeConfig,
24
+ RouterInputType,
25
+ RouterNode,
26
+ RouterNodeConfig,
27
+ RouterRule,
28
+ StartNode,
29
+ StartNodeConfig,
30
+ TemplateNode,
31
+ TemplateNodeConfig,
32
+ input_ports,
33
+ output_ports,
34
+ ports_compatible,
35
+ prompt_variables,
36
+ single_required_string_input,
37
+ )
38
+ from agentplane_core.interfaces import ScoredHit, SearchBackend, SecretsProvider, VectorStore
39
+ from agentplane_core.registry import (
40
+ AuthMode,
41
+ Capabilities,
42
+ Card,
43
+ DefinitionInfo,
44
+ DefinitionStatus,
45
+ DeploymentInfo,
46
+ EntryKind,
47
+ HealthStatus,
48
+ Page,
49
+ RegistryEntry,
50
+ RegistryEntryCreate,
51
+ RegistryEntryPatch,
52
+ SearchQuery,
53
+ Severity,
54
+ ToolCard,
55
+ ToolCardTool,
56
+ ValidationIssue,
57
+ ValidationResult,
58
+ serialize_card,
59
+ )
60
+ from agentplane_core.resources import (
61
+ SECRET_PLACEHOLDER,
62
+ VECTOR_DB_KINDS,
63
+ EmbeddingConfig,
64
+ McpServerResource,
65
+ ModelProviderResource,
66
+ Resource,
67
+ ResourceKind,
68
+ VectorDBResource,
69
+ )
70
+ from agentplane_core.types import (
71
+ Document,
72
+ JsonObject,
73
+ JsonSchema,
74
+ PortType,
75
+ render_documents,
76
+ split_port_ref,
77
+ )
78
+ from agentplane_core.validation import ErrorCode, validate_structure
79
+
80
+ __version__ = "0.0.1"
81
+
82
+ __all__ = [
83
+ "DEPRECATED_NODE_VERSIONS",
84
+ "NODE_CATALOG",
85
+ "SCHEMA_VERSION",
86
+ "SECRET_PLACEHOLDER",
87
+ "VECTOR_DB_KINDS",
88
+ "AgentCapabilities",
89
+ "AgentCard",
90
+ "AgentInterface",
91
+ "AgentSkill",
92
+ "AuthMode",
93
+ "Capabilities",
94
+ "Card",
95
+ "DefinitionInfo",
96
+ "DefinitionStatus",
97
+ "DeploymentInfo",
98
+ "Document",
99
+ "Edge",
100
+ "EmbeddingConfig",
101
+ "EndNode",
102
+ "EndNodeConfig",
103
+ "EntryKind",
104
+ "ErrorCode",
105
+ "ExposeConfig",
106
+ "FlowDefinition",
107
+ "HealthStatus",
108
+ "JsonObject",
109
+ "JsonSchema",
110
+ "Layout",
111
+ "LayoutPosition",
112
+ "LlmCallNode",
113
+ "LlmCallNodeConfig",
114
+ "McpServerResource",
115
+ "McpToolNode",
116
+ "McpToolNodeConfig",
117
+ "ModelProviderResource",
118
+ "Node",
119
+ "Page",
120
+ "PortType",
121
+ "RegistryEntry",
122
+ "RegistryEntryCreate",
123
+ "RegistryEntryPatch",
124
+ "Resource",
125
+ "ResourceKind",
126
+ "RetrievalNode",
127
+ "RetrievalNodeConfig",
128
+ "RouterInputType",
129
+ "RouterNode",
130
+ "RouterNodeConfig",
131
+ "RouterRule",
132
+ "ScoredHit",
133
+ "SearchBackend",
134
+ "SearchQuery",
135
+ "SecretsProvider",
136
+ "Severity",
137
+ "StartNode",
138
+ "StartNodeConfig",
139
+ "TemplateNode",
140
+ "TemplateNodeConfig",
141
+ "ToolCard",
142
+ "ToolCardTool",
143
+ "ValidationIssue",
144
+ "ValidationResult",
145
+ "VectorDBResource",
146
+ "VectorStore",
147
+ "agent_card_from_dict",
148
+ "agent_card_to_json_dict",
149
+ "input_ports",
150
+ "output_ports",
151
+ "ports_compatible",
152
+ "prompt_variables",
153
+ "render_documents",
154
+ "serialize_card",
155
+ "single_required_string_input",
156
+ "split_port_ref",
157
+ "validate_structure",
158
+ ]
@@ -0,0 +1,34 @@
1
+ """AgentCard <-> A2A JSON wire format.
2
+
3
+ The official ``a2a.types.AgentCard`` is a protobuf message; its JSON wire
4
+ format (camelCase, per A2A v1.0) is produced/consumed with the protobuf JSON
5
+ mapping. ``google.protobuf`` ships with ``a2a-sdk`` — no extra dependency.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import cast
11
+
12
+ from a2a.types import AgentCard
13
+ from google.protobuf.json_format import MessageToDict, ParseDict
14
+
15
+ from agentplane_core.types import JsonObject
16
+
17
+
18
+ def agent_card_to_json_dict(card: AgentCard) -> JsonObject:
19
+ """Serialize an AgentCard to its A2A JSON object form."""
20
+ return cast(JsonObject, MessageToDict(card))
21
+
22
+
23
+ def agent_card_from_dict(data: JsonObject) -> AgentCard:
24
+ """Parse an A2A JSON card into the official protobuf AgentCard.
25
+
26
+ Unknown fields (e.g. v0.3 compatibility fields some servers still emit)
27
+ are ignored.
28
+ """
29
+ card = AgentCard()
30
+ ParseDict(data, card, ignore_unknown_fields=True)
31
+ return card
32
+
33
+
34
+ __all__ = ["agent_card_from_dict", "agent_card_to_json_dict"]
@@ -0,0 +1,411 @@
1
+ """The Definition schema — the platform's single artifact type (SPEC §3.1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Annotated, Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
9
+
10
+ from agentplane_core.types import (
11
+ BranchName,
12
+ JsonObject,
13
+ JsonSchema,
14
+ NodeId,
15
+ PortType,
16
+ Slug,
17
+ ToolName,
18
+ split_port_ref,
19
+ )
20
+
21
+ SCHEMA_VERSION = 1
22
+
23
+ # (type, version) pairs the platform knows how to execute. Unknown pairs fail
24
+ # validation with E002. Append-only per node-type version.
25
+ NODE_CATALOG: frozenset[tuple[str, int]] = frozenset(
26
+ {
27
+ ("start", 1),
28
+ ("end", 1),
29
+ ("llm_call", 1),
30
+ ("mcp_tool", 1),
31
+ ("retrieval", 1),
32
+ # v1.1 additions
33
+ ("router", 1),
34
+ ("template", 1),
35
+ }
36
+ )
37
+
38
+ # (type, version) pairs that still run but warn with W001 on validation.
39
+ DEPRECATED_NODE_VERSIONS: frozenset[tuple[str, int]] = frozenset()
40
+
41
+
42
+ class _StrictModel(BaseModel):
43
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
44
+
45
+
46
+ class ExposeConfig(_StrictModel):
47
+ """How the runtime serves a flow: as an A2A agent or as an MCP server."""
48
+
49
+ model_config = ConfigDict(extra="forbid", frozen=True)
50
+
51
+ kind: Literal["a2a", "mcp"]
52
+ tool_name: ToolName | None = None
53
+ tool_description: str = ""
54
+
55
+
56
+ class StartNodeConfig(_StrictModel):
57
+ model_config = ConfigDict(extra="forbid", frozen=True)
58
+
59
+ input_schema: JsonSchema
60
+
61
+
62
+ class EndNodeConfig(_StrictModel):
63
+ model_config = ConfigDict(extra="forbid", frozen=True)
64
+
65
+ output_from: str = Field(
66
+ default="",
67
+ description=(
68
+ "Reference 'node_id.port' the flow output is read from. Empty means: "
69
+ "take the value wired into the `input` port — required for branched "
70
+ "flows where different runs feed `end` from different nodes."
71
+ ),
72
+ )
73
+
74
+
75
+ class LlmCallNodeConfig(_StrictModel):
76
+ model_config = ConfigDict(extra="forbid", frozen=True)
77
+
78
+ resource: Slug
79
+ model: str = ""
80
+ prompt: str
81
+ system_prompt: str = ""
82
+ structured_output: JsonSchema | None = None
83
+ stream: bool = False
84
+
85
+
86
+ class McpToolNodeConfig(_StrictModel):
87
+ model_config = ConfigDict(extra="forbid", frozen=True)
88
+
89
+ resource: Slug | None = None
90
+ url: str | None = None
91
+ tool: str
92
+ args: dict[str, str] = Field(
93
+ default_factory=dict, description="input port name -> tool argument name"
94
+ )
95
+
96
+
97
+ class RetrievalNodeConfig(_StrictModel):
98
+ model_config = ConfigDict(extra="forbid", frozen=True)
99
+
100
+ resource: Slug
101
+ collection: str
102
+ top_k: int = Field(default=4, ge=1, le=100)
103
+ filter: JsonObject | None = None
104
+
105
+
106
+ RouterInputType = Literal["text", "json", "documents"]
107
+
108
+
109
+ class RouterRule(_StrictModel):
110
+ """One branch condition, evaluated in order; first match wins."""
111
+
112
+ model_config = ConfigDict(extra="forbid", frozen=True)
113
+
114
+ when: Literal["not_empty", "empty"]
115
+ branch: BranchName
116
+
117
+
118
+ class RouterNodeConfig(_StrictModel):
119
+ """Conditional branching (v1.1): routes the input value to one branch.
120
+
121
+ Every rule branch plus the default branch becomes a typed output port
122
+ that passes the input value through; only the chosen branch's downstream
123
+ nodes execute.
124
+ """
125
+
126
+ model_config = ConfigDict(extra="forbid", frozen=True)
127
+
128
+ input_type: RouterInputType = "text"
129
+ rules: list[RouterRule] = Field(min_length=1)
130
+ default_branch: BranchName = "otherwise"
131
+
132
+
133
+ class TemplateNodeConfig(_StrictModel):
134
+ """Static/interpolated text (v1.1): `{vars}` become input ports.
135
+
136
+ The `trigger` input port exists on every template so a router branch can
137
+ activate it even when the text needs no variables (fallback messages).
138
+ """
139
+
140
+ model_config = ConfigDict(extra="forbid", frozen=True)
141
+
142
+ text: str
143
+
144
+
145
+ class _NodeBase(_StrictModel):
146
+ model_config = ConfigDict(extra="forbid", frozen=True)
147
+
148
+ id: NodeId
149
+ version: int = Field(ge=1)
150
+
151
+
152
+ class StartNode(_NodeBase):
153
+ type: Literal["start"]
154
+ config: StartNodeConfig
155
+
156
+
157
+ class EndNode(_NodeBase):
158
+ type: Literal["end"]
159
+ config: EndNodeConfig
160
+
161
+
162
+ class LlmCallNode(_NodeBase):
163
+ type: Literal["llm_call"]
164
+ config: LlmCallNodeConfig
165
+
166
+
167
+ class McpToolNode(_NodeBase):
168
+ type: Literal["mcp_tool"]
169
+ config: McpToolNodeConfig
170
+
171
+
172
+ class RetrievalNode(_NodeBase):
173
+ type: Literal["retrieval"]
174
+ config: RetrievalNodeConfig
175
+
176
+
177
+ class RouterNode(_NodeBase):
178
+ type: Literal["router"]
179
+ config: RouterNodeConfig
180
+
181
+
182
+ class TemplateNode(_NodeBase):
183
+ type: Literal["template"]
184
+ config: TemplateNodeConfig
185
+
186
+
187
+ Node = Annotated[
188
+ StartNode | EndNode | LlmCallNode | McpToolNode | RetrievalNode | RouterNode | TemplateNode,
189
+ Field(discriminator="type"),
190
+ ]
191
+
192
+
193
+ class Edge(_StrictModel):
194
+ model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
195
+
196
+ from_: str = Field(alias="from", serialization_alias="from")
197
+ to: str
198
+
199
+
200
+ class LayoutPosition(BaseModel):
201
+ """Canvas position — builder-owned, ignored by the runtime."""
202
+
203
+ model_config = ConfigDict(extra="allow", frozen=True)
204
+
205
+ x: float = 0
206
+ y: float = 0
207
+
208
+
209
+ class Layout(BaseModel):
210
+ """Canvas layout block — builder-owned, ignored by the runtime."""
211
+
212
+ model_config = ConfigDict(extra="allow", frozen=True)
213
+
214
+ nodes: dict[str, LayoutPosition] = Field(default_factory=dict)
215
+
216
+
217
+ class FlowDefinition(_StrictModel):
218
+ """Versioned, serializable description of a flow (SPEC §3.1).
219
+
220
+ Serialization is deterministic: nodes are sorted by id, edges by
221
+ (from, to), keys stay in model order — ``parse(serialize(d)) == d``.
222
+ """
223
+
224
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
225
+
226
+ schema_version: int
227
+ name: Slug
228
+ display_name: str = ""
229
+ description: str = ""
230
+ tags: list[str] = Field(default_factory=list)
231
+ expose: ExposeConfig
232
+ nodes: list[Node]
233
+ edges: list[Edge] = Field(default_factory=list)
234
+ layout: Layout | None = None
235
+
236
+ @model_validator(mode="before")
237
+ @classmethod
238
+ def _canonical_order(cls, data: object) -> object:
239
+ """Sort nodes by id and edges by (from, to) for deterministic serialization."""
240
+ if not isinstance(data, dict):
241
+ return data
242
+
243
+ def _node_key(item: object) -> str:
244
+ if isinstance(item, dict):
245
+ node_id = item.get("id", "")
246
+ return node_id if isinstance(node_id, str) else ""
247
+ return getattr(item, "id", "")
248
+
249
+ def _edge_key(item: object) -> tuple[str, str]:
250
+ if isinstance(item, dict):
251
+ from_ = item.get("from", item.get("from_", ""))
252
+ to = item.get("to", "")
253
+ return (from_ if isinstance(from_, str) else "", to if isinstance(to, str) else "")
254
+ return (getattr(item, "from_", ""), getattr(item, "to", ""))
255
+
256
+ nodes = data.get("nodes")
257
+ if isinstance(nodes, list):
258
+ data = {**data, "nodes": sorted(nodes, key=_node_key)}
259
+ edges = data.get("edges")
260
+ if isinstance(edges, list):
261
+ data = {**data, "edges": sorted(edges, key=_edge_key)}
262
+ return data
263
+
264
+ def node_map(self) -> dict[str, Node]:
265
+ """Nodes keyed by id (last one wins on duplicates; E040 catches those)."""
266
+ return {node.id: node for node in self.nodes}
267
+
268
+ def canonical_dict(self) -> JsonObject:
269
+ """Deterministic JSON-mode dump used for export and hashing."""
270
+ dumped: JsonObject = self.model_dump(mode="json", by_alias=True, exclude_none=True)
271
+ return dumped
272
+
273
+
274
+ # `{vars}` in prompts become named input ports of that node. Double braces
275
+ # (`{{literal}}`) are not ports.
276
+ _PROMPT_VAR_RE = re.compile(r"(?<!\{)\{([a-z_][a-z0-9_]*)\}(?!\})")
277
+
278
+
279
+ def prompt_variables(template: str) -> list[str]:
280
+ """Extract `{var}` input ports from a prompt template, in first-seen order."""
281
+ seen: dict[str, None] = {}
282
+ for match in _PROMPT_VAR_RE.finditer(template):
283
+ seen.setdefault(match.group(1))
284
+ return list(seen)
285
+
286
+
287
+ def _schema_property_port_type(prop_schema: object) -> PortType:
288
+ if isinstance(prop_schema, dict) and prop_schema.get("type") == "string":
289
+ return "text"
290
+ return "json"
291
+
292
+
293
+ def output_ports(node: Node) -> dict[str, PortType]: # noqa: PLR0911 - one return per node type
294
+ """Typed output ports a node exposes."""
295
+ match node:
296
+ case StartNode():
297
+ props = node.config.input_schema.get("properties")
298
+ if not isinstance(props, dict):
299
+ return {}
300
+ return {name: _schema_property_port_type(schema) for name, schema in props.items()}
301
+ case EndNode():
302
+ return {}
303
+ case LlmCallNode():
304
+ ports: dict[str, PortType] = {"text": "text"}
305
+ if node.config.structured_output is not None:
306
+ ports["json"] = "json"
307
+ return ports
308
+ case McpToolNode():
309
+ return {"result": "json"}
310
+ case RetrievalNode():
311
+ return {"documents": "documents"}
312
+ case RouterNode():
313
+ branches: dict[str, PortType] = {
314
+ rule.branch: node.config.input_type for rule in node.config.rules
315
+ }
316
+ branches[node.config.default_branch] = node.config.input_type
317
+ return branches
318
+ case TemplateNode():
319
+ return {"text": "text"}
320
+
321
+
322
+ def input_ports(node: Node) -> dict[str, PortType]: # noqa: PLR0911 - one return per node type
323
+ """Typed input ports a node accepts."""
324
+ match node:
325
+ case StartNode():
326
+ return {}
327
+ case EndNode():
328
+ return {"input": "text"}
329
+ case LlmCallNode():
330
+ names = prompt_variables(node.config.prompt) + prompt_variables(
331
+ node.config.system_prompt
332
+ )
333
+ return dict.fromkeys(names, "text")
334
+ case McpToolNode():
335
+ return dict.fromkeys(node.config.args, "json")
336
+ case RetrievalNode():
337
+ return {"query": "text"}
338
+ case RouterNode():
339
+ return {"input": node.config.input_type}
340
+ case TemplateNode():
341
+ template_ports: dict[str, PortType] = {"trigger": "text"}
342
+ for name in prompt_variables(node.config.text):
343
+ template_ports[name] = "text"
344
+ return template_ports
345
+
346
+
347
+ def ports_compatible(src: PortType, dst: PortType) -> bool:
348
+ """Port type compatibility matrix (SPEC §3.1).
349
+
350
+ Identical types always connect. ``documents -> text`` is rendered
351
+ implicitly (concatenated chunks with source headers); ``json -> text``
352
+ is stringified; ``text -> json`` is a plain JSON string value.
353
+ """
354
+ if src == dst:
355
+ return True
356
+ return (src, dst) in {("documents", "text"), ("json", "text"), ("text", "json")}
357
+
358
+
359
+ def single_required_string_input(defn: FlowDefinition) -> str | None:
360
+ """Name of the single required string property of ``start.input_schema``, if any.
361
+
362
+ Used for A2A input binding: plain message text binds to this property
363
+ (SPEC §6.4); otherwise the message must be a JSON object.
364
+ """
365
+ start = next((n for n in defn.nodes if isinstance(n, StartNode)), None)
366
+ if start is None:
367
+ return None
368
+ schema = start.config.input_schema
369
+ required = schema.get("required")
370
+ props = schema.get("properties")
371
+ if not isinstance(required, list) or len(required) != 1 or not isinstance(props, dict):
372
+ return None
373
+ name = required[0]
374
+ if not isinstance(name, str):
375
+ return None
376
+ return name if _schema_property_port_type(props.get(name)) == "text" else None
377
+
378
+
379
+ __all__ = [
380
+ "DEPRECATED_NODE_VERSIONS",
381
+ "NODE_CATALOG",
382
+ "SCHEMA_VERSION",
383
+ "Edge",
384
+ "EndNode",
385
+ "EndNodeConfig",
386
+ "ExposeConfig",
387
+ "FlowDefinition",
388
+ "Layout",
389
+ "LayoutPosition",
390
+ "LlmCallNode",
391
+ "LlmCallNodeConfig",
392
+ "McpToolNode",
393
+ "McpToolNodeConfig",
394
+ "Node",
395
+ "RetrievalNode",
396
+ "RetrievalNodeConfig",
397
+ "RouterInputType",
398
+ "RouterNode",
399
+ "RouterNodeConfig",
400
+ "RouterRule",
401
+ "StartNode",
402
+ "StartNodeConfig",
403
+ "TemplateNode",
404
+ "TemplateNodeConfig",
405
+ "input_ports",
406
+ "output_ports",
407
+ "ports_compatible",
408
+ "prompt_variables",
409
+ "single_required_string_input",
410
+ "split_port_ref",
411
+ ]