@compilr-dev/sdk 0.10.9 → 0.10.11
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/dist/capabilities/packs.js +8 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/skills/software-skills.js +2 -1
- package/dist/system-prompt/modules.js +1 -1
- package/dist/team/skill-requirements.js +15 -3
- package/dist/team/tool-config.js +1 -0
- package/dist/team/types.js +2 -2
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/interactive-flow-tool.d.ts +246 -0
- package/dist/tools/interactive-flow-tool.js +393 -0
- package/package.json +1 -1
|
@@ -53,11 +53,17 @@ export const CAPABILITY_PACKS = {
|
|
|
53
53
|
interaction: {
|
|
54
54
|
id: 'interaction',
|
|
55
55
|
label: 'User Interaction',
|
|
56
|
-
tools: [
|
|
56
|
+
tools: [
|
|
57
|
+
'ask_user',
|
|
58
|
+
'ask_user_simple',
|
|
59
|
+
'propose_alternatives',
|
|
60
|
+
'suggest',
|
|
61
|
+
'build_interactive_flow',
|
|
62
|
+
],
|
|
57
63
|
readOnly: true,
|
|
58
64
|
promptModules: [],
|
|
59
65
|
estimatedPromptTokens: 0,
|
|
60
|
-
estimatedToolTokens:
|
|
66
|
+
estimatedToolTokens: 1100,
|
|
61
67
|
},
|
|
62
68
|
coordinator: {
|
|
63
69
|
id: 'coordinator',
|
package/dist/index.d.ts
CHANGED
|
@@ -56,8 +56,8 @@ export type { SystemPromptContext, BuildResult, SystemPromptModule, ModuleCondit
|
|
|
56
56
|
export type { ProjectType, ProjectStatus, RepoPattern, WorkflowMode, LifecycleState, WorkItemType, WorkItemStatus, WorkItemPriority, GuidedStep, DocumentType, PlanStatus, Project, WorkItem, ProjectDocument, Plan, PlanSummary, PlanWithWorkItem, HistoryEntry, CreateProjectInput, UpdateProjectInput, ProjectListOptions, CreateWorkItemInput, UpdateWorkItemInput, QueryWorkItemsInput, CreateDocumentInput, UpdateDocumentInput, CreatePlanInput, UpdatePlanInput, ListPlansOptions, WorkItemQueryResult, ProjectListResult, BulkCreateItem, WorkItemComment, CreateCommentInput, UpdateCommentInput, IProjectRepository, IWorkItemRepository, IDocumentRepository, IPlanRepository, ICommentRepository, IAnchorService, IArtifactService, IEpisodeService, AnchorData, ArtifactType, ArtifactData, ArtifactSummaryData, WorkEpisode, ProjectWorkSummary, PlatformContext, PlatformToolsConfig, PlatformHooks, StepCriteria, } from './platform/index.js';
|
|
57
57
|
export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemRepository, SQLiteDocumentRepository, SQLitePlanRepository, SQLiteCommentRepository, getDatabase, closeDatabase, closeAllDatabases, databaseExists, SCHEMA_VERSION, SCHEMA_SQL, } from './platform/index.js';
|
|
58
58
|
export type { SQLiteRepositories, CreateSQLiteRepositoriesOptions, ProjectDeleteHooks, ProjectRecord, WorkItemRecord, ProjectDocumentRecord, WorkItemCommentRecord, } from './platform/index.js';
|
|
59
|
-
export { createAskUserTool, createAskUserSimpleTool, createProposeAlternativesTool, } from './tools/index.js';
|
|
60
|
-
export type { AskUserQuestion, AskUserInput, AskUserResult, AskUserHandler, AskUserSimpleInput, AskUserSimpleResult, AskUserSimpleHandler, Alternative, ProposeAlternativesInput, ProposeAlternativesResult, ProposeAlternativesHandler, } from './tools/index.js';
|
|
59
|
+
export { createAskUserTool, createAskUserSimpleTool, createProposeAlternativesTool, createInteractiveFlowTool, validateFlow, INTERACTIVE_FLOW_INPUT_SCHEMA, } from './tools/index.js';
|
|
60
|
+
export type { AskUserQuestion, AskUserInput, AskUserResult, AskUserHandler, AskUserSimpleInput, AskUserSimpleResult, AskUserSimpleHandler, Alternative, ProposeAlternativesInput, ProposeAlternativesResult, ProposeAlternativesHandler, Flow, FlowTone, FlowNode, QuestionNode, InfoNode, BranchNode, SummaryNode, QuestionInput, Choice, Proposal, NextRef, BranchRoute, BranchCondition, NodeId, IconName, Tint, RenderVariant, AnswerValue, InteractiveFlowInput, InteractiveFlowResult, InteractiveFlowHandler, FlowValidationResult, FlowValidationError, FlowErrorCode, } from './tools/index.js';
|
|
61
61
|
export { EntitlementCache, UNLIMITED, OFFLINE_FALLBACK_LIMITS, DailyCounter, formatLimitMessage, formatTimeUntilReset, formatUpgradeHint, } from './entitlements/index.js';
|
|
62
62
|
export type { TierLimits, EntitlementResponse, LimitCheckResult, IEntitlementStore, EntitlementCacheConfig, } from './entitlements/index.js';
|
|
63
63
|
export { detectProject, suggestProjectType, detectCommon } from './detection/index.js';
|
package/dist/index.js
CHANGED
|
@@ -131,7 +131,7 @@ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemReposi
|
|
|
131
131
|
// =============================================================================
|
|
132
132
|
// User Interaction Tools (ask_user, ask_user_simple)
|
|
133
133
|
// =============================================================================
|
|
134
|
-
export { createAskUserTool, createAskUserSimpleTool, createProposeAlternativesTool, } from './tools/index.js';
|
|
134
|
+
export { createAskUserTool, createAskUserSimpleTool, createProposeAlternativesTool, createInteractiveFlowTool, validateFlow, INTERACTIVE_FLOW_INPUT_SCHEMA, } from './tools/index.js';
|
|
135
135
|
// =============================================================================
|
|
136
136
|
// Entitlements (server-driven tier management)
|
|
137
137
|
// =============================================================================
|
|
@@ -148,13 +148,14 @@ For complex features:
|
|
|
148
148
|
|
|
149
149
|
- **ask_user_simple**: For single-choice questions (preferred for most interactions)
|
|
150
150
|
- **ask_user**: For multi-question batches (when gathering related info)
|
|
151
|
+
- **build_interactive_flow**: For decisions with 2+ branching considerations the user benefits from exploring visually (navigable tree with Back/forward; agent receives the full reasoning path back)
|
|
151
152
|
- **backlog_read**: Use id param to get specific item, use search/filters otherwise
|
|
152
153
|
- **backlog_write**: Update items after refinement decisions
|
|
153
154
|
- **todo_write**: Track your progress through refinement
|
|
154
155
|
|
|
155
156
|
## RULES
|
|
156
157
|
|
|
157
|
-
1. ALWAYS use ask_user_simple or
|
|
158
|
+
1. ALWAYS use ask_user_simple, ask_user, or build_interactive_flow for decisions - NEVER ask questions in plain text
|
|
158
159
|
2. Keep backlog reads efficient - use filters and limits
|
|
159
160
|
3. One refinement focus at a time
|
|
160
161
|
4. Update backlog with backlog_write after each decision
|
|
@@ -140,7 +140,7 @@ IMPORTANT: Tool names are lowercase with underscores.
|
|
|
140
140
|
- **File operations**: read_file, write_file, edit, glob, grep
|
|
141
141
|
- **Shell**: bash (use run_in_background=true for long-running), bash_output, kill_shell
|
|
142
142
|
- **Task management**: todo_write, todo_read
|
|
143
|
-
- **User interaction**:
|
|
143
|
+
- **User interaction**: ask_user_simple (single question, small models), ask_user (1-5 questions), propose_alternatives (2-3 options with pros/cons), build_interactive_flow (navigable decision tree — use when a choice has 2+ branching considerations the user benefits from exploring visually with Back/forward)
|
|
144
144
|
- **Sub-agents**: task (types: explore, code-review, plan, debug, test-runner, refactor, security-audit, general)`,
|
|
145
145
|
};
|
|
146
146
|
/**
|
|
@@ -46,12 +46,18 @@ export const SKILL_REQUIREMENTS = {
|
|
|
46
46
|
// Planning skills
|
|
47
47
|
planning: {
|
|
48
48
|
required: ['read_file', 'glob'],
|
|
49
|
-
optional: ['ask_user', 'ask_user_simple', 'todo_write'],
|
|
49
|
+
optional: ['ask_user', 'ask_user_simple', 'build_interactive_flow', 'todo_write'],
|
|
50
50
|
reason: 'Needs to understand codebase to plan effectively',
|
|
51
51
|
},
|
|
52
52
|
design: {
|
|
53
53
|
required: ['ask_user', 'workitem_add'],
|
|
54
|
-
optional: [
|
|
54
|
+
optional: [
|
|
55
|
+
'propose_alternatives',
|
|
56
|
+
'build_interactive_flow',
|
|
57
|
+
'detect_project',
|
|
58
|
+
'document_save',
|
|
59
|
+
'edit',
|
|
60
|
+
],
|
|
55
61
|
reason: 'Needs to gather requirements and create backlog',
|
|
56
62
|
},
|
|
57
63
|
refine: {
|
|
@@ -72,7 +78,13 @@ export const SKILL_REQUIREMENTS = {
|
|
|
72
78
|
// Documentation skills
|
|
73
79
|
architecture: {
|
|
74
80
|
required: ['read_file', 'write_file', 'edit'],
|
|
75
|
-
optional: [
|
|
81
|
+
optional: [
|
|
82
|
+
'backlog_read',
|
|
83
|
+
'ask_user',
|
|
84
|
+
'propose_alternatives',
|
|
85
|
+
'build_interactive_flow',
|
|
86
|
+
'glob',
|
|
87
|
+
],
|
|
76
88
|
reason: 'Creates architecture documents',
|
|
77
89
|
},
|
|
78
90
|
prd: {
|
package/dist/team/tool-config.js
CHANGED
|
@@ -22,6 +22,7 @@ const TOOL_NAMES = {
|
|
|
22
22
|
ASK_USER: 'ask_user',
|
|
23
23
|
ASK_USER_SIMPLE: 'ask_user_simple',
|
|
24
24
|
PROPOSE_ALTERNATIVES: 'propose_alternatives',
|
|
25
|
+
BUILD_INTERACTIVE_FLOW: 'build_interactive_flow',
|
|
25
26
|
// Delegation
|
|
26
27
|
DELEGATE: 'delegate',
|
|
27
28
|
DELEGATE_BACKGROUND: 'delegate_background',
|
package/dist/team/types.js
CHANGED
|
@@ -115,7 +115,7 @@ You are the **Project Manager (PM)** in this multi-agent development team. You s
|
|
|
115
115
|
|
|
116
116
|
**Direct tools** (call by name):
|
|
117
117
|
- \`todo_write\`, \`todo_read\` - Task tracking
|
|
118
|
-
- \`ask_user\`, \`suggest\` - User interaction
|
|
118
|
+
- \`ask_user_simple\`, \`ask_user\`, \`propose_alternatives\`, \`build_interactive_flow\`, \`suggest\` - User interaction
|
|
119
119
|
- \`handoff\` - Hand off to another specialist
|
|
120
120
|
|
|
121
121
|
**Specialized tools** (call via \`use_tool\`):
|
|
@@ -601,7 +601,7 @@ You are the **Business Analyst** in this multi-agent development team. You trans
|
|
|
601
601
|
|
|
602
602
|
**Direct tools** (call by name):
|
|
603
603
|
- \`todo_write\`, \`todo_read\` - Task tracking
|
|
604
|
-
- \`ask_user\`, \`suggest\` - User interaction
|
|
604
|
+
- \`ask_user_simple\`, \`ask_user\`, \`propose_alternatives\`, \`build_interactive_flow\`, \`suggest\` - User interaction
|
|
605
605
|
- \`handoff\` - Hand off to another specialist
|
|
606
606
|
|
|
607
607
|
**Specialized tools** (call via \`use_tool\`):
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -6,5 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { createAskUserTool, createAskUserSimpleTool } from './ask-user-tools.js';
|
|
8
8
|
export { createProposeAlternativesTool } from './propose-alternatives-tool.js';
|
|
9
|
+
export { createInteractiveFlowTool, validateFlow, INTERACTIVE_FLOW_INPUT_SCHEMA, } from './interactive-flow-tool.js';
|
|
9
10
|
export type { AskUserQuestion, AskUserInput, AskUserResult, AskUserHandler, AskUserSimpleInput, AskUserSimpleResult, AskUserSimpleHandler, } from './ask-user-tools.js';
|
|
10
11
|
export type { Alternative, ProposeAlternativesInput, ProposeAlternativesResult, ProposeAlternativesHandler, } from './propose-alternatives-tool.js';
|
|
12
|
+
export type { Flow, FlowTone, Node as FlowNode, QuestionNode, InfoNode, BranchNode, SummaryNode, QuestionInput, Choice, Proposal, NextRef, BranchRoute, BranchCondition, NodeId, IconName, Tint, RenderVariant, AnswerValue, InteractiveFlowInput, InteractiveFlowResult, InteractiveFlowHandler, FlowValidationResult, FlowValidationError, FlowErrorCode, } from './interactive-flow-tool.js';
|
package/dist/tools/index.js
CHANGED
|
@@ -6,3 +6,4 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { createAskUserTool, createAskUserSimpleTool } from './ask-user-tools.js';
|
|
8
8
|
export { createProposeAlternativesTool } from './propose-alternatives-tool.js';
|
|
9
|
+
export { createInteractiveFlowTool, validateFlow, INTERACTIVE_FLOW_INPUT_SCHEMA, } from './interactive-flow-tool.js';
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Flow Tool — Agent-authored navigable decision trees
|
|
3
|
+
*
|
|
4
|
+
* The agent builds a JSON DSL describing a tree of nodes; the consumer
|
|
5
|
+
* (Desktop, eventually CLI) renders the flow as a modal/screen-stack the user
|
|
6
|
+
* navigates. The user's path through the tree + final answers are returned to
|
|
7
|
+
* the agent as a structured artifact.
|
|
8
|
+
*
|
|
9
|
+
* Same factory pattern as ask-user-tools.ts: SDK defines the schema/types/
|
|
10
|
+
* validator; the consumer provides the UI handler.
|
|
11
|
+
*
|
|
12
|
+
* Spec: project-docs/00-requirements/compilr-dev-desktop/interactive-flow-dsl-spec.md
|
|
13
|
+
* Plan: project-docs/00-requirements/compilr-dev-desktop/interactive-flow-dsl-implementation-plan.md
|
|
14
|
+
*/
|
|
15
|
+
import type { Tool } from '@compilr-dev/agents';
|
|
16
|
+
/** Unique identifier for a node within a flow */
|
|
17
|
+
export type NodeId = string;
|
|
18
|
+
/** Top-level flow definition the agent authors */
|
|
19
|
+
export interface Flow {
|
|
20
|
+
/** Title shown in the modal header */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Optional subhead under the title */
|
|
23
|
+
description?: string;
|
|
24
|
+
/** ID of the first node to render */
|
|
25
|
+
startNode: NodeId;
|
|
26
|
+
/** All nodes in the flow, keyed by ID */
|
|
27
|
+
nodes: Record<NodeId, Node>;
|
|
28
|
+
/** Motion style — see spec §5; default 'default' */
|
|
29
|
+
tone?: FlowTone;
|
|
30
|
+
/** Visual density — default 'comfortable' */
|
|
31
|
+
density?: 'compact' | 'comfortable';
|
|
32
|
+
}
|
|
33
|
+
/** Motion intensity. 'minimal' = opacity fades only (also forced by reduced-motion). */
|
|
34
|
+
export type FlowTone = 'default' | 'minimal';
|
|
35
|
+
/** Node union — the four primitives */
|
|
36
|
+
export type Node = QuestionNode | InfoNode | BranchNode | SummaryNode;
|
|
37
|
+
/** Ask the user for input */
|
|
38
|
+
export interface QuestionNode {
|
|
39
|
+
type: 'question';
|
|
40
|
+
id: NodeId;
|
|
41
|
+
prompt: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
input: QuestionInput;
|
|
44
|
+
/** Optional renderer override; consumer infers from input shape if omitted */
|
|
45
|
+
render?: RenderVariant;
|
|
46
|
+
next: NextRef;
|
|
47
|
+
}
|
|
48
|
+
/** Show information; no input */
|
|
49
|
+
export interface InfoNode {
|
|
50
|
+
type: 'info';
|
|
51
|
+
id: NodeId;
|
|
52
|
+
title: string;
|
|
53
|
+
/** Markdown body */
|
|
54
|
+
body: string;
|
|
55
|
+
render?: RenderVariant;
|
|
56
|
+
next: NextRef;
|
|
57
|
+
}
|
|
58
|
+
/** Pure routing logic — no UI */
|
|
59
|
+
export interface BranchNode {
|
|
60
|
+
type: 'branch';
|
|
61
|
+
id: NodeId;
|
|
62
|
+
routes: BranchRoute[];
|
|
63
|
+
/** Fallback target if no route matches */
|
|
64
|
+
default: NodeId;
|
|
65
|
+
}
|
|
66
|
+
/** Terminal recap — flow ends when user confirms */
|
|
67
|
+
export interface SummaryNode {
|
|
68
|
+
type: 'summary';
|
|
69
|
+
id: NodeId;
|
|
70
|
+
title: string;
|
|
71
|
+
/** Optional markdown shown above the answer table */
|
|
72
|
+
recap?: string;
|
|
73
|
+
render?: RenderVariant;
|
|
74
|
+
}
|
|
75
|
+
/** Routing rule for a BranchNode */
|
|
76
|
+
export interface BranchRoute {
|
|
77
|
+
when: BranchCondition;
|
|
78
|
+
goto: NodeId;
|
|
79
|
+
}
|
|
80
|
+
/** Condition language is intentionally tiny in v1 — no &&/|| */
|
|
81
|
+
export type BranchCondition = {
|
|
82
|
+
questionId: NodeId;
|
|
83
|
+
equals: string;
|
|
84
|
+
} | {
|
|
85
|
+
questionId: NodeId;
|
|
86
|
+
includes: string;
|
|
87
|
+
} | {
|
|
88
|
+
questionId: NodeId;
|
|
89
|
+
notEquals: string;
|
|
90
|
+
};
|
|
91
|
+
/** Question input modes — each maps to a set of compatible renderers */
|
|
92
|
+
export type QuestionInput = {
|
|
93
|
+
mode: 'single';
|
|
94
|
+
choices: Choice[];
|
|
95
|
+
} | {
|
|
96
|
+
mode: 'multi';
|
|
97
|
+
choices: Choice[];
|
|
98
|
+
min?: number;
|
|
99
|
+
max?: number;
|
|
100
|
+
} | {
|
|
101
|
+
mode: 'text';
|
|
102
|
+
placeholder?: string;
|
|
103
|
+
multiline?: boolean;
|
|
104
|
+
} | {
|
|
105
|
+
mode: 'proposal';
|
|
106
|
+
options: Proposal[];
|
|
107
|
+
};
|
|
108
|
+
/** A selectable choice for single/multi modes */
|
|
109
|
+
export interface Choice {
|
|
110
|
+
id: string;
|
|
111
|
+
label: string;
|
|
112
|
+
description?: string;
|
|
113
|
+
icon?: IconName;
|
|
114
|
+
tint?: Tint;
|
|
115
|
+
}
|
|
116
|
+
/** A proposal with pros/cons for proposal mode */
|
|
117
|
+
export interface Proposal {
|
|
118
|
+
id: string;
|
|
119
|
+
label: string;
|
|
120
|
+
pros?: string[];
|
|
121
|
+
cons?: string[];
|
|
122
|
+
icon?: IconName;
|
|
123
|
+
tint?: Tint;
|
|
124
|
+
}
|
|
125
|
+
/** Where to go after this node */
|
|
126
|
+
export type NextRef = NodeId | {
|
|
127
|
+
byAnswer: Record<string, NodeId>;
|
|
128
|
+
default?: NodeId;
|
|
129
|
+
};
|
|
130
|
+
/** Lucide icon name (kebab-case slug per lucide.dev URLs) */
|
|
131
|
+
export type IconName = string;
|
|
132
|
+
/** Semantic color hint — consumer maps to theme colors */
|
|
133
|
+
export type Tint = 'accent' | 'success' | 'warning' | 'danger' | 'muted';
|
|
134
|
+
/**
|
|
135
|
+
* Renderer name — specific to node type and (for questions) input mode.
|
|
136
|
+
* Consumer-side dispatcher uses this name to look up the React component.
|
|
137
|
+
* Validation here only checks that the renderer is compatible with the node's
|
|
138
|
+
* mode (e.g., 'card-grid' on a text-mode question is rejected).
|
|
139
|
+
*/
|
|
140
|
+
export type RenderVariant = string;
|
|
141
|
+
export interface InteractiveFlowInput {
|
|
142
|
+
flow: Flow;
|
|
143
|
+
}
|
|
144
|
+
/** Value collected from a node (Question only — info/branch/summary don't produce values) */
|
|
145
|
+
export type AnswerValue = string | string[];
|
|
146
|
+
export interface InteractiveFlowResult {
|
|
147
|
+
/** False if the user aborted (closed modal, switched conversation, etc.) */
|
|
148
|
+
completed: boolean;
|
|
149
|
+
/** Every node visited, in order — including backtracked nodes */
|
|
150
|
+
path: NodeId[];
|
|
151
|
+
/** Final answers — answers downstream of a backtrack point are wiped */
|
|
152
|
+
answers: Record<NodeId, AnswerValue>;
|
|
153
|
+
/** Present iff !completed — the node where the user aborted */
|
|
154
|
+
abortedAt?: NodeId;
|
|
155
|
+
/** Wall-clock time the user spent */
|
|
156
|
+
durationMs: number;
|
|
157
|
+
/** Validator warnings that didn't block execution (e.g., orphan nodes) */
|
|
158
|
+
warnings?: string[];
|
|
159
|
+
}
|
|
160
|
+
/** UI handler — the consumer provides this; the SDK calls it once validation passes */
|
|
161
|
+
export type InteractiveFlowHandler = (input: InteractiveFlowInput) => Promise<InteractiveFlowResult>;
|
|
162
|
+
/** Error codes — see spec §4 */
|
|
163
|
+
export type FlowErrorCode = 'MISSING_START_NODE' | 'DUPLICATE_NODE_ID' | 'UNRESOLVED_NEXT' | 'UNRESOLVED_BRANCH' | 'INVALID_ICON' | 'INVALID_RENDERER' | 'INVALID_NODE_TYPE' | 'INVALID_CONDITION';
|
|
164
|
+
export interface FlowValidationError {
|
|
165
|
+
code: FlowErrorCode;
|
|
166
|
+
message: string;
|
|
167
|
+
/** Node where the error was found, if applicable */
|
|
168
|
+
nodeId?: NodeId;
|
|
169
|
+
}
|
|
170
|
+
export interface FlowValidationResult {
|
|
171
|
+
ok: boolean;
|
|
172
|
+
errors: FlowValidationError[];
|
|
173
|
+
warnings: string[];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Validate a flow. Accepts `unknown` because inputs flow in as JSON from the
|
|
177
|
+
* agent and TS types are lies at the boundary — runtime defensive checks
|
|
178
|
+
* happen before the cast to Flow.
|
|
179
|
+
*/
|
|
180
|
+
export declare function validateFlow(flowInput: unknown): FlowValidationResult;
|
|
181
|
+
/**
|
|
182
|
+
* Create the `build_interactive_flow` tool with a custom UI handler.
|
|
183
|
+
*
|
|
184
|
+
* The SDK validates the input flow (schema + cross-references). If validation
|
|
185
|
+
* fails, the tool returns an error to the agent without invoking the handler.
|
|
186
|
+
* If validation passes, the handler is called and its Promise is awaited.
|
|
187
|
+
* Warnings (e.g., orphan nodes) are passed through in the result.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* // Desktop: send IPC to renderer
|
|
192
|
+
* const flowTool = createInteractiveFlowTool(async (input) => {
|
|
193
|
+
* return await sendInteractiveFlowToRenderer(input);
|
|
194
|
+
* });
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export declare function createInteractiveFlowTool(handler: InteractiveFlowHandler): Tool<InteractiveFlowInput>;
|
|
198
|
+
/**
|
|
199
|
+
* JSON Schema for `build_interactive_flow` input. Validates structural shape;
|
|
200
|
+
* cross-reference validation (next/branch/icon/renderer) happens in validateFlow().
|
|
201
|
+
*
|
|
202
|
+
* The schema is deliberately permissive on `nodes` (object<string, object>)
|
|
203
|
+
* rather than enumerating each node-type shape — the variant validation lives
|
|
204
|
+
* in code where errors carry more context for the agent to act on.
|
|
205
|
+
*/
|
|
206
|
+
export declare const INTERACTIVE_FLOW_INPUT_SCHEMA: {
|
|
207
|
+
type: "object";
|
|
208
|
+
properties: {
|
|
209
|
+
flow: {
|
|
210
|
+
type: string;
|
|
211
|
+
properties: {
|
|
212
|
+
title: {
|
|
213
|
+
type: string;
|
|
214
|
+
description: string;
|
|
215
|
+
};
|
|
216
|
+
description: {
|
|
217
|
+
type: string;
|
|
218
|
+
description: string;
|
|
219
|
+
};
|
|
220
|
+
startNode: {
|
|
221
|
+
type: string;
|
|
222
|
+
description: string;
|
|
223
|
+
};
|
|
224
|
+
nodes: {
|
|
225
|
+
type: string;
|
|
226
|
+
description: string;
|
|
227
|
+
additionalProperties: {
|
|
228
|
+
type: string;
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
tone: {
|
|
232
|
+
type: string;
|
|
233
|
+
enum: string[];
|
|
234
|
+
description: string;
|
|
235
|
+
};
|
|
236
|
+
density: {
|
|
237
|
+
type: string;
|
|
238
|
+
enum: string[];
|
|
239
|
+
description: string;
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
required: string[];
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
required: string[];
|
|
246
|
+
};
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Flow Tool — Agent-authored navigable decision trees
|
|
3
|
+
*
|
|
4
|
+
* The agent builds a JSON DSL describing a tree of nodes; the consumer
|
|
5
|
+
* (Desktop, eventually CLI) renders the flow as a modal/screen-stack the user
|
|
6
|
+
* navigates. The user's path through the tree + final answers are returned to
|
|
7
|
+
* the agent as a structured artifact.
|
|
8
|
+
*
|
|
9
|
+
* Same factory pattern as ask-user-tools.ts: SDK defines the schema/types/
|
|
10
|
+
* validator; the consumer provides the UI handler.
|
|
11
|
+
*
|
|
12
|
+
* Spec: project-docs/00-requirements/compilr-dev-desktop/interactive-flow-dsl-spec.md
|
|
13
|
+
* Plan: project-docs/00-requirements/compilr-dev-desktop/interactive-flow-dsl-implementation-plan.md
|
|
14
|
+
*/
|
|
15
|
+
import { defineTool } from '@compilr-dev/agents';
|
|
16
|
+
/** Renderers compatible with each input mode (consumer-side defaults documented in spec §3) */
|
|
17
|
+
const QUESTION_RENDERERS = {
|
|
18
|
+
single: ['radio', 'tile-row', 'segmented', 'card-grid'],
|
|
19
|
+
multi: ['checkbox-list', 'tag-cloud', 'card-grid-multi'],
|
|
20
|
+
text: ['single-line', 'multiline', 'code'],
|
|
21
|
+
proposal: ['stacked-cards', 'comparison-table', 'detailed-cards'],
|
|
22
|
+
};
|
|
23
|
+
const INFO_RENDERERS = [
|
|
24
|
+
'callout',
|
|
25
|
+
'detail-card',
|
|
26
|
+
'bullet-list',
|
|
27
|
+
'numbered-steps',
|
|
28
|
+
'comparison',
|
|
29
|
+
'warning',
|
|
30
|
+
'success',
|
|
31
|
+
];
|
|
32
|
+
const SUMMARY_RENDERERS = ['compact', 'detailed'];
|
|
33
|
+
/**
|
|
34
|
+
* Lucide icon names use kebab-case slugs (e.g., 'database', 'smartphone',
|
|
35
|
+
* 'a-arrow-down'). v1 validation is FORMAT-only: lowercase letters, digits,
|
|
36
|
+
* and single hyphens. A future iteration will load the actual Lucide catalog
|
|
37
|
+
* and reject unknown slugs; for now Desktop falls back gracefully when a
|
|
38
|
+
* slug doesn't map to a component.
|
|
39
|
+
*/
|
|
40
|
+
const ICON_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
41
|
+
/**
|
|
42
|
+
* Validate a flow. Accepts `unknown` because inputs flow in as JSON from the
|
|
43
|
+
* agent and TS types are lies at the boundary — runtime defensive checks
|
|
44
|
+
* happen before the cast to Flow.
|
|
45
|
+
*/
|
|
46
|
+
export function validateFlow(flowInput) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
const warnings = [];
|
|
49
|
+
// Defensive: missing top-level fields
|
|
50
|
+
if (!flowInput || typeof flowInput !== 'object') {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
errors: [{ code: 'INVALID_NODE_TYPE', message: 'Flow must be an object' }],
|
|
54
|
+
warnings,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const flowMaybe = flowInput;
|
|
58
|
+
if (!flowMaybe.nodes || typeof flowMaybe.nodes !== 'object') {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
errors: [{ code: 'INVALID_NODE_TYPE', message: 'Flow.nodes must be an object' }],
|
|
62
|
+
warnings,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// After both runtime gates, it's safe to narrow to Flow for the rest of the function.
|
|
66
|
+
const flow = flowInput;
|
|
67
|
+
const nodeIds = Object.keys(flow.nodes);
|
|
68
|
+
// 1. startNode must exist
|
|
69
|
+
if (!flow.startNode || !(flow.startNode in flow.nodes)) {
|
|
70
|
+
errors.push({
|
|
71
|
+
code: 'MISSING_START_NODE',
|
|
72
|
+
message: `startNode '${flow.startNode}' is not defined in flow.nodes`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// 2. Each node's id must match the key it lives under
|
|
76
|
+
for (const id of nodeIds) {
|
|
77
|
+
const node = flow.nodes[id];
|
|
78
|
+
if (node.id !== id) {
|
|
79
|
+
errors.push({
|
|
80
|
+
code: 'DUPLICATE_NODE_ID',
|
|
81
|
+
message: `Node key '${id}' does not match node.id '${node.id}'`,
|
|
82
|
+
nodeId: id,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 3. Per-node validation: type, references, icons, renderers, conditions
|
|
87
|
+
for (const id of nodeIds) {
|
|
88
|
+
const node = flow.nodes[id];
|
|
89
|
+
validateNode(node, flow.nodes, errors);
|
|
90
|
+
}
|
|
91
|
+
// 4. Reachability — warn on orphans
|
|
92
|
+
if (errors.length === 0 && flow.startNode in flow.nodes) {
|
|
93
|
+
const reachable = computeReachable(flow);
|
|
94
|
+
for (const id of nodeIds) {
|
|
95
|
+
if (!reachable.has(id)) {
|
|
96
|
+
warnings.push(`Node '${id}' is unreachable from startNode`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
101
|
+
}
|
|
102
|
+
function validateNode(node, allNodes, errors) {
|
|
103
|
+
switch (node.type) {
|
|
104
|
+
case 'question':
|
|
105
|
+
validateQuestion(node, allNodes, errors);
|
|
106
|
+
break;
|
|
107
|
+
case 'info':
|
|
108
|
+
validateInfo(node, allNodes, errors);
|
|
109
|
+
break;
|
|
110
|
+
case 'branch':
|
|
111
|
+
validateBranch(node, allNodes, errors);
|
|
112
|
+
break;
|
|
113
|
+
case 'summary':
|
|
114
|
+
validateSummary(node, errors);
|
|
115
|
+
break;
|
|
116
|
+
default: {
|
|
117
|
+
// Defensive — TypeScript should prevent this but JSON inputs can be malformed
|
|
118
|
+
const unknown = node;
|
|
119
|
+
errors.push({
|
|
120
|
+
code: 'INVALID_NODE_TYPE',
|
|
121
|
+
message: `Unknown node type '${String(unknown.type)}'`,
|
|
122
|
+
nodeId: unknown.id,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function validateQuestion(node, allNodes, errors) {
|
|
128
|
+
// Input mode must have a valid renderer if specified. The lookup CAN miss
|
|
129
|
+
// at runtime if the agent passed an unknown mode in the JSON — TS narrowing
|
|
130
|
+
// doesn't help us across the boundary, hence the explicit widened type.
|
|
131
|
+
const validRenderers = QUESTION_RENDERERS[node.input.mode];
|
|
132
|
+
if (!validRenderers) {
|
|
133
|
+
errors.push({
|
|
134
|
+
code: 'INVALID_NODE_TYPE',
|
|
135
|
+
message: `Unknown question input mode '${node.input.mode}'`,
|
|
136
|
+
nodeId: node.id,
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (node.render && !validRenderers.includes(node.render)) {
|
|
141
|
+
errors.push({
|
|
142
|
+
code: 'INVALID_RENDERER',
|
|
143
|
+
message: `Renderer '${node.render}' is not valid for ${node.input.mode}-mode questions (valid: ${validRenderers.join(', ')})`,
|
|
144
|
+
nodeId: node.id,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Validate icons on choices/proposals
|
|
148
|
+
if (node.input.mode === 'single' || node.input.mode === 'multi') {
|
|
149
|
+
for (const choice of node.input.choices) {
|
|
150
|
+
if (choice.icon !== undefined)
|
|
151
|
+
checkIcon(choice.icon, node.id, errors);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (node.input.mode === 'proposal') {
|
|
155
|
+
for (const opt of node.input.options) {
|
|
156
|
+
if (opt.icon !== undefined)
|
|
157
|
+
checkIcon(opt.icon, node.id, errors);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Validate next references
|
|
161
|
+
validateNextRef(node.next, node, allNodes, errors);
|
|
162
|
+
}
|
|
163
|
+
function validateInfo(node, allNodes, errors) {
|
|
164
|
+
if (node.render && !INFO_RENDERERS.includes(node.render)) {
|
|
165
|
+
errors.push({
|
|
166
|
+
code: 'INVALID_RENDERER',
|
|
167
|
+
message: `Renderer '${node.render}' is not valid for info nodes (valid: ${INFO_RENDERERS.join(', ')})`,
|
|
168
|
+
nodeId: node.id,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
validateNextRef(node.next, node, allNodes, errors);
|
|
172
|
+
}
|
|
173
|
+
function validateBranch(node, allNodes, errors) {
|
|
174
|
+
// All routes must reference existing nodes and use valid conditions
|
|
175
|
+
for (let i = 0; i < node.routes.length; i++) {
|
|
176
|
+
const route = node.routes[i];
|
|
177
|
+
if (!isValidCondition(route.when)) {
|
|
178
|
+
errors.push({
|
|
179
|
+
code: 'INVALID_CONDITION',
|
|
180
|
+
message: `Branch route ${String(i)} has an invalid condition shape`,
|
|
181
|
+
nodeId: node.id,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (!(route.goto in allNodes)) {
|
|
185
|
+
errors.push({
|
|
186
|
+
code: 'UNRESOLVED_BRANCH',
|
|
187
|
+
message: `Branch route ${String(i)} goto '${route.goto}' is not defined in flow.nodes`,
|
|
188
|
+
nodeId: node.id,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!node.default || !(node.default in allNodes)) {
|
|
193
|
+
errors.push({
|
|
194
|
+
code: 'UNRESOLVED_BRANCH',
|
|
195
|
+
message: `Branch default '${node.default}' is not defined in flow.nodes`,
|
|
196
|
+
nodeId: node.id,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function validateSummary(node, errors) {
|
|
201
|
+
if (node.render && !SUMMARY_RENDERERS.includes(node.render)) {
|
|
202
|
+
errors.push({
|
|
203
|
+
code: 'INVALID_RENDERER',
|
|
204
|
+
message: `Renderer '${node.render}' is not valid for summary nodes (valid: ${SUMMARY_RENDERERS.join(', ')})`,
|
|
205
|
+
nodeId: node.id,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// Summary has no next — flow terminates here
|
|
209
|
+
}
|
|
210
|
+
function validateNextRef(next, node, allNodes, errors) {
|
|
211
|
+
if (typeof next === 'string') {
|
|
212
|
+
if (!(next in allNodes)) {
|
|
213
|
+
errors.push({
|
|
214
|
+
code: 'UNRESOLVED_NEXT',
|
|
215
|
+
message: `Node '${node.id}' next references '${next}' which is not defined in flow.nodes`,
|
|
216
|
+
nodeId: node.id,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// byAnswer object
|
|
222
|
+
for (const [answer, target] of Object.entries(next.byAnswer)) {
|
|
223
|
+
if (!(target in allNodes)) {
|
|
224
|
+
errors.push({
|
|
225
|
+
code: 'UNRESOLVED_NEXT',
|
|
226
|
+
message: `Node '${node.id}' next.byAnswer['${answer}'] references '${target}' which is not defined in flow.nodes`,
|
|
227
|
+
nodeId: node.id,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (next.default !== undefined && !(next.default in allNodes)) {
|
|
232
|
+
errors.push({
|
|
233
|
+
code: 'UNRESOLVED_NEXT',
|
|
234
|
+
message: `Node '${node.id}' next.default references '${next.default}' which is not defined in flow.nodes`,
|
|
235
|
+
nodeId: node.id,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function isValidCondition(condition) {
|
|
240
|
+
// Accepts unknown because the agent's JSON might pass anything; TS narrows BranchCondition
|
|
241
|
+
// too tightly to validate the runtime shape.
|
|
242
|
+
if (!condition || typeof condition !== 'object')
|
|
243
|
+
return false;
|
|
244
|
+
const c = condition;
|
|
245
|
+
if (typeof c.questionId !== 'string')
|
|
246
|
+
return false;
|
|
247
|
+
return (typeof c.equals === 'string' ||
|
|
248
|
+
typeof c.includes === 'string' ||
|
|
249
|
+
typeof c.notEquals === 'string');
|
|
250
|
+
}
|
|
251
|
+
function checkIcon(icon, nodeId, errors) {
|
|
252
|
+
if (!ICON_NAME_RE.test(icon)) {
|
|
253
|
+
errors.push({
|
|
254
|
+
code: 'INVALID_ICON',
|
|
255
|
+
message: `Icon '${icon}' is not a valid Lucide slug (lowercase kebab-case, e.g., 'database', 'smartphone', 'a-arrow-down')`,
|
|
256
|
+
nodeId,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Compute set of node IDs reachable from startNode via any path */
|
|
261
|
+
function computeReachable(flow) {
|
|
262
|
+
const reachable = new Set();
|
|
263
|
+
const stack = [flow.startNode];
|
|
264
|
+
while (stack.length > 0) {
|
|
265
|
+
const id = stack.pop();
|
|
266
|
+
if (!id || reachable.has(id) || !(id in flow.nodes))
|
|
267
|
+
continue;
|
|
268
|
+
reachable.add(id);
|
|
269
|
+
const node = flow.nodes[id];
|
|
270
|
+
switch (node.type) {
|
|
271
|
+
case 'question':
|
|
272
|
+
case 'info':
|
|
273
|
+
addNextTargets(node.next, stack);
|
|
274
|
+
break;
|
|
275
|
+
case 'branch':
|
|
276
|
+
for (const route of node.routes)
|
|
277
|
+
stack.push(route.goto);
|
|
278
|
+
stack.push(node.default);
|
|
279
|
+
break;
|
|
280
|
+
case 'summary':
|
|
281
|
+
// terminal
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return reachable;
|
|
286
|
+
}
|
|
287
|
+
function addNextTargets(next, stack) {
|
|
288
|
+
if (typeof next === 'string') {
|
|
289
|
+
stack.push(next);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
for (const target of Object.values(next.byAnswer))
|
|
293
|
+
stack.push(target);
|
|
294
|
+
if (next.default !== undefined)
|
|
295
|
+
stack.push(next.default);
|
|
296
|
+
}
|
|
297
|
+
// =============================================================================
|
|
298
|
+
// Factory
|
|
299
|
+
// =============================================================================
|
|
300
|
+
/**
|
|
301
|
+
* Create the `build_interactive_flow` tool with a custom UI handler.
|
|
302
|
+
*
|
|
303
|
+
* The SDK validates the input flow (schema + cross-references). If validation
|
|
304
|
+
* fails, the tool returns an error to the agent without invoking the handler.
|
|
305
|
+
* If validation passes, the handler is called and its Promise is awaited.
|
|
306
|
+
* Warnings (e.g., orphan nodes) are passed through in the result.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```typescript
|
|
310
|
+
* // Desktop: send IPC to renderer
|
|
311
|
+
* const flowTool = createInteractiveFlowTool(async (input) => {
|
|
312
|
+
* return await sendInteractiveFlowToRenderer(input);
|
|
313
|
+
* });
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
export function createInteractiveFlowTool(handler) {
|
|
317
|
+
return defineTool({
|
|
318
|
+
name: 'build_interactive_flow',
|
|
319
|
+
description: 'Render a navigable decision tree (modal wizard) to the user. ' +
|
|
320
|
+
'Use this when a decision branches into multiple paths that benefit from ' +
|
|
321
|
+
'visual exploration — the user can move forward, go back, and the agent ' +
|
|
322
|
+
'gets back both their answers and the path they took through the tree. ' +
|
|
323
|
+
'Prefer over ask_user when the decision has 2+ branching considerations.',
|
|
324
|
+
inputSchema: INTERACTIVE_FLOW_INPUT_SCHEMA,
|
|
325
|
+
execute: async (input) => {
|
|
326
|
+
try {
|
|
327
|
+
const validation = validateFlow(input.flow);
|
|
328
|
+
if (!validation.ok) {
|
|
329
|
+
const summary = validation.errors
|
|
330
|
+
.map((e) => `[${e.code}]${e.nodeId ? ` (node '${e.nodeId}')` : ''} ${e.message}`)
|
|
331
|
+
.join('\n');
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
error: `Interactive flow validation failed:\n${summary}`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const result = await handler(input);
|
|
338
|
+
if (validation.warnings.length > 0) {
|
|
339
|
+
result.warnings = [...(result.warnings ?? []), ...validation.warnings];
|
|
340
|
+
}
|
|
341
|
+
return { success: true, result };
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
return {
|
|
345
|
+
success: false,
|
|
346
|
+
error: `Failed to run interactive flow: ${err instanceof Error ? err.message : String(err)}`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
silent: true,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
// =============================================================================
|
|
354
|
+
// JSON Schema (for the agent's tool registration)
|
|
355
|
+
// =============================================================================
|
|
356
|
+
/**
|
|
357
|
+
* JSON Schema for `build_interactive_flow` input. Validates structural shape;
|
|
358
|
+
* cross-reference validation (next/branch/icon/renderer) happens in validateFlow().
|
|
359
|
+
*
|
|
360
|
+
* The schema is deliberately permissive on `nodes` (object<string, object>)
|
|
361
|
+
* rather than enumerating each node-type shape — the variant validation lives
|
|
362
|
+
* in code where errors carry more context for the agent to act on.
|
|
363
|
+
*/
|
|
364
|
+
export const INTERACTIVE_FLOW_INPUT_SCHEMA = {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
flow: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
properties: {
|
|
370
|
+
title: { type: 'string', description: 'Modal header title' },
|
|
371
|
+
description: { type: 'string', description: 'Optional subhead under the title' },
|
|
372
|
+
startNode: { type: 'string', description: 'ID of the first node to render' },
|
|
373
|
+
nodes: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
description: 'Map of node ID → node. Each node has a `type` field: question, info, branch, or summary. See tool documentation for the full DSL schema.',
|
|
376
|
+
additionalProperties: { type: 'object' },
|
|
377
|
+
},
|
|
378
|
+
tone: {
|
|
379
|
+
type: 'string',
|
|
380
|
+
enum: ['default', 'minimal'],
|
|
381
|
+
description: 'Motion style; default "default"',
|
|
382
|
+
},
|
|
383
|
+
density: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
enum: ['compact', 'comfortable'],
|
|
386
|
+
description: 'Visual density; default "comfortable"',
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
required: ['title', 'startNode', 'nodes'],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
required: ['flow'],
|
|
393
|
+
};
|