Skip to content

Commit 73a0c78

Browse files
committed
[refactor] Enhance ConditionalTask and WhileTask configuration handling
- Updated ConditionalTask to include a new optional `conditionConfig` property in its configuration schema, allowing for more flexible branch conditions. - Refactored WhileTask to remove reliance on `extras` for condition fields, integrating them directly into the task configuration for improved clarity and usability. - Adjusted related types and serialization structures to accommodate the new configuration format. - Updated tests to validate the new configuration handling for both ConditionalTask and WhileTask, ensuring consistent behavior across task executions.
1 parent 7607335 commit 73a0c78

6 files changed

Lines changed: 79 additions & 109 deletions

File tree

packages/task-graph/src/task/ConditionalTask.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,18 @@ export const conditionalTaskConfigSchema = {
9898
branches: { type: "array", items: {} },
9999
defaultBranch: { type: "string" },
100100
exclusive: { type: "boolean" },
101+
conditionConfig: { type: "object", additionalProperties: true },
101102
},
102103
additionalProperties: false,
103104
} as const satisfies DataPortSchema;
104105

105106
export type ConditionalTaskConfig = TaskConfig & {
106107
/** Branches may contain ConditionFn functions — not JSON-schema-representable */
107-
readonly branches: BranchConfig<any>[];
108+
readonly branches?: BranchConfig<any>[];
108109
readonly defaultBranch?: string;
109110
readonly exclusive?: boolean;
111+
/** Serializable UI condition configuration used to build branches at runtime. */
112+
readonly conditionConfig?: UIConditionConfig;
110113
};
111114

112115
// ============================================================================
@@ -283,10 +286,10 @@ export class ConditionalTask<
283286
};
284287
}
285288

286-
// Try to find serialized conditionConfig from input or extras
289+
// Try to find serialized conditionConfig from input or config
287290
const conditionConfig =
288291
((input as Record<string, unknown>).conditionConfig as UIConditionConfig | undefined) ??
289-
(this.config.extras?.conditionConfig as UIConditionConfig | undefined);
292+
this.config.conditionConfig;
290293

291294
if (conditionConfig) {
292295
return {

packages/task-graph/src/task/TaskJSON.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { TaskGraph } from "../task-graph/TaskGraph";
99
import { CompoundMergeStrategy } from "../task-graph/TaskGraphRunner";
1010
import { TaskConfigurationError, TaskJSONError } from "../task/TaskError";
1111
import { TaskRegistry } from "../task/TaskRegistry";
12+
import { ConditionalTaskConfig } from "./ConditionalTask";
1213
import { GraphAsTask } from "./GraphAsTask";
14+
import { IteratorTaskConfig } from "./IteratorTask";
1315
import { TaskConfig, TaskInput } from "./TaskTypes";
16+
import { WhileTaskConfig } from "./WhileTask";
1417

1518
// ========================================================================
1619
// JSON Serialization Types
@@ -20,6 +23,11 @@ import { TaskConfig, TaskInput } from "./TaskTypes";
2023
* This structure defines how tasks should be configured in JSON format.
2124
*/
2225

26+
export type JsonTaskConfig = Omit<
27+
TaskConfig & WhileTaskConfig & IteratorTaskConfig & ConditionalTaskConfig,
28+
"id"
29+
>;
30+
2331
export type JsonTaskItem = {
2432
/** Unique identifier for the task */
2533
id: unknown;
@@ -28,7 +36,7 @@ export type JsonTaskItem = {
2836
type: string;
2937

3038
/** Optional configuration for the task */
31-
config?: Omit<TaskConfig, "id"> & Record<string, unknown>;
39+
config?: JsonTaskConfig;
3240

3341
/** Default input values for the task */
3442
defaults?: TaskInput;
@@ -61,7 +69,7 @@ export type TaskGraphItemJson = {
6169
id: unknown;
6270
type: string;
6371
defaults?: TaskInput;
64-
config?: Omit<TaskConfig, "id"> & Record<string, unknown>;
72+
config?: JsonTaskConfig;
6573
subgraph?: TaskGraphJson;
6674
merge?: CompoundMergeStrategy;
6775
};

packages/task-graph/src/task/TaskTypes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ export const TaskConfigSchema = {
9494
cacheable: { type: "boolean" },
9595
inputSchema: { type: "object", properties: {}, additionalProperties: true },
9696
outputSchema: { type: "object", properties: {}, additionalProperties: true },
97-
extras: { type: "object", additionalProperties: true },
97+
extras: {
98+
type: "object",
99+
additionalProperties: true,
100+
},
98101
},
99102
additionalProperties: false,
100103
} as const satisfies DataPortSchema;

packages/task-graph/src/task/WhileTask.ts

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export const whileTaskConfigSchema = {
4949
condition: {},
5050
maxIterations: { type: "integer", minimum: 1 },
5151
chainIterations: { type: "boolean" },
52+
conditionField: { type: "string" },
53+
conditionOperator: { type: "string" },
54+
conditionValue: { type: "string" },
55+
iterationInputConfig: { type: "object", additionalProperties: true },
5256
},
5357
additionalProperties: false,
5458
} as const satisfies DataPortSchema;
@@ -76,6 +80,18 @@ export type WhileTaskConfig<Output extends TaskOutput = TaskOutput> = GraphAsTas
7680
* @default true
7781
*/
7882
readonly chainIterations?: boolean;
83+
84+
/** Output field to evaluate for the loop condition. */
85+
readonly conditionField?: string;
86+
87+
/** Comparison operator for the loop condition. */
88+
readonly conditionOperator?: string;
89+
90+
/** Value to compare against for the loop condition. */
91+
readonly conditionValue?: string;
92+
93+
/** Per-property iteration input configuration (scalar/array/flexible). */
94+
readonly iterationInputConfig?: Record<string, { mode: string; baseSchema?: unknown }>;
7995
};
8096

8197
/**
@@ -184,22 +200,16 @@ export class WhileTask<
184200

185201
/**
186202
* Gets the maximum iterations limit.
187-
* Falls back to extras.whileConfig.maxIterations for JSON-deserialized tasks.
188203
*/
189204
public get maxIterations(): number {
190-
if (this.config.maxIterations !== undefined) return this.config.maxIterations;
191-
const wc = this.config.extras?.whileConfig as { maxIterations?: number } | undefined;
192-
return wc?.maxIterations ?? 100;
205+
return this.config.maxIterations ?? 100;
193206
}
194207

195208
/**
196209
* Whether to chain iteration outputs to inputs.
197-
* Falls back to extras.whileConfig.chainIterations for JSON-deserialized tasks.
198210
*/
199211
public get chainIterations(): boolean {
200-
if (this.config.chainIterations !== undefined) return this.config.chainIterations;
201-
const wc = this.config.extras?.whileConfig as { chainIterations?: boolean } | undefined;
202-
return wc?.chainIterations ?? true;
212+
return this.config.chainIterations ?? true;
203213
}
204214

205215
/**
@@ -217,20 +227,15 @@ export class WhileTask<
217227
* Execute the while loop.
218228
*/
219229
/**
220-
* Builds a condition function from the serialized whileConfig in extras
221-
* when no condition function is directly provided in config.
230+
* Builds a condition function from the serialized condition fields in config.
222231
*/
223-
private buildConditionFromExtras(): WhileConditionFn<Output> | undefined {
224-
const wc = this.config.extras?.whileConfig as
225-
| { conditionField?: string; conditionOperator?: string; conditionValue?: string }
226-
| undefined;
232+
private buildConditionFromConfig(): WhileConditionFn<Output> | undefined {
233+
const { conditionOperator, conditionField, conditionValue } = this.config;
227234

228-
if (!wc?.conditionOperator) {
235+
if (!conditionOperator) {
229236
return undefined;
230237
}
231238

232-
const { conditionField, conditionOperator, conditionValue } = wc;
233-
234239
return (output: Output) => {
235240
const fieldValue = conditionField
236241
? getNestedValue(output as Record<string, unknown>, conditionField)
@@ -251,16 +256,12 @@ export class WhileTask<
251256
iteratedValues: Record<string, unknown[]>;
252257
iterationCount: number;
253258
} | null {
254-
const wc = this.config.extras?.whileConfig as
255-
| { iterationInputConfig?: Record<string, { mode: string; baseSchema?: unknown }> }
256-
| undefined;
257-
258-
if (!wc?.iterationInputConfig) {
259+
if (!this.config.iterationInputConfig) {
259260
return null;
260261
}
261262

262263
const inputData = input as Record<string, unknown>;
263-
const config = wc.iterationInputConfig;
264+
const config = this.config.iterationInputConfig!;
264265

265266
const arrayPorts: string[] = [];
266267
const scalarPorts: string[] = [];
@@ -350,7 +351,7 @@ export class WhileTask<
350351
}
351352

352353
// Use provided condition or auto-build from serialized whileConfig
353-
const condition = this.condition ?? this.buildConditionFromExtras();
354+
const condition = this.condition ?? this.buildConditionFromConfig();
354355

355356
if (!condition) {
356357
throw new TaskConfigurationError(`${this.type}: No condition function provided`);
@@ -426,15 +427,12 @@ export class WhileTask<
426427
* This provides streaming output for the final result while still
427428
* supporting iteration chaining.
428429
*/
429-
async *executeStream(
430-
input: Input,
431-
context: IExecuteContext
432-
): AsyncIterable<StreamEvent<Output>> {
430+
async *executeStream(input: Input, context: IExecuteContext): AsyncIterable<StreamEvent<Output>> {
433431
if (!this.hasChildren()) {
434432
throw new TaskConfigurationError(`${this.type}: No subgraph set for while loop`);
435433
}
436434

437-
const condition = this.condition ?? this.buildConditionFromExtras();
435+
const condition = this.condition ?? this.buildConditionFromConfig();
438436
if (!condition) {
439437
throw new TaskConfigurationError(`${this.type}: No condition function provided`);
440438
}
@@ -550,19 +548,15 @@ export class WhileTask<
550548
const baseSchema = super.inputSchema();
551549
if (typeof baseSchema === "boolean") return baseSchema;
552550

553-
const wc = this.config.extras?.whileConfig as
554-
| { iterationInputConfig?: Record<string, { mode: string; baseSchema?: DataPortSchema }> }
555-
| undefined;
556-
557-
if (!wc?.iterationInputConfig) {
551+
if (!this.config.iterationInputConfig) {
558552
return baseSchema;
559553
}
560554

561555
// Wrap array-mode ports in anyOf (scalar | array) schemas.
562556
// Using anyOf instead of plain type:"array" to avoid addInput's array-merge behavior
563557
// which would prepend an undefined element when runInputData starts empty.
564558
const properties = { ...(baseSchema.properties || {}) } as Record<string, DataPortSchema>;
565-
for (const [key, propConfig] of Object.entries(wc.iterationInputConfig)) {
559+
for (const [key, propConfig] of Object.entries(this.config.iterationInputConfig)) {
566560
if (propConfig.mode === "array" && properties[key]) {
567561
const scalarSchema = properties[key] as DataPortSchema;
568562
properties[key] = {

packages/test/src/test/task/ConditionalTaskCondition.test.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -343,21 +343,18 @@ describe("ConditionalTask with serialized conditionConfig", () => {
343343
});
344344
});
345345

346-
describe("conditionConfig from extras", () => {
347-
it("should use conditionConfig from extras when input has none", async () => {
348-
const conditionConfig: UIConditionConfig = {
349-
branches: [
350-
{ id: "high", field: "value", operator: "greater_than", value: "50" },
351-
{ id: "low", field: "value", operator: "less_or_equal", value: "50" },
352-
],
353-
exclusive: true,
354-
};
355-
346+
describe("conditionConfig from config", () => {
347+
it("should use conditionConfig from config when input has none", async () => {
356348
const task = new ConditionalTask(
357349
{},
358350
{
359-
branches: [],
360-
extras: { conditionConfig },
351+
conditionConfig: {
352+
branches: [
353+
{ id: "high", field: "value", operator: "greater_than", value: "50" },
354+
{ id: "low", field: "value", operator: "less_or_equal", value: "50" },
355+
],
356+
exclusive: true,
357+
},
361358
}
362359
);
363360

packages/test/src/test/task/WhileTaskCondition.test.ts

Lines changed: 21 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,9 @@ describe("WhileTask with serialized whileConfig", () => {
2020
.while({
2121
maxIterations: 20,
2222
chainIterations: true,
23-
extras: {
24-
whileConfig: {
25-
conditionField: "quality",
26-
conditionOperator: "less_than",
27-
conditionValue: "0.9",
28-
},
29-
},
23+
conditionField: "quality",
24+
conditionOperator: "less_than",
25+
conditionValue: "0.9",
3026
})
3127
.addTask(RefineTask)
3228
.endWhile();
@@ -46,13 +42,9 @@ describe("WhileTask with serialized whileConfig", () => {
4642
.while({
4743
maxIterations: 20,
4844
chainIterations: true,
49-
extras: {
50-
whileConfig: {
51-
conditionField: "quality",
52-
conditionOperator: "not_equals",
53-
conditionValue: "1",
54-
},
55-
},
45+
conditionField: "quality",
46+
conditionOperator: "not_equals",
47+
conditionValue: "1",
5648
})
5749
.addTask(RefineTask)
5850
.endWhile();
@@ -70,13 +62,9 @@ describe("WhileTask with serialized whileConfig", () => {
7062
.while({
7163
maxIterations: 20,
7264
chainIterations: true,
73-
extras: {
74-
whileConfig: {
75-
conditionField: "quality",
76-
conditionOperator: "equals",
77-
conditionValue: "0.2",
78-
},
79-
},
65+
conditionField: "quality",
66+
conditionOperator: "equals",
67+
conditionValue: "0.2",
8068
})
8169
.addTask(RefineTask)
8270
.endWhile();
@@ -96,13 +84,9 @@ describe("WhileTask with serialized whileConfig", () => {
9684
.while({
9785
maxIterations: 3,
9886
chainIterations: true,
99-
extras: {
100-
whileConfig: {
101-
conditionField: "quality",
102-
conditionOperator: "less_than",
103-
conditionValue: "999", // always true
104-
},
105-
},
87+
conditionField: "quality",
88+
conditionOperator: "less_than",
89+
conditionValue: "999",
10690
})
10791
.addTask(RefineTask)
10892
.endWhile();
@@ -122,13 +106,9 @@ describe("WhileTask with serialized whileConfig", () => {
122106
.while({
123107
maxIterations: 3,
124108
chainIterations: true,
125-
extras: {
126-
whileConfig: {
127-
conditionField: "quality",
128-
conditionOperator: "less_than",
129-
conditionValue: "999",
130-
},
131-
},
109+
conditionField: "quality",
110+
conditionOperator: "less_than",
111+
conditionValue: "999",
132112
})
133113
.addTask(RefineTask)
134114
.endWhile();
@@ -148,13 +128,9 @@ describe("WhileTask with serialized whileConfig", () => {
148128
.while({
149129
maxIterations: 3,
150130
chainIterations: false,
151-
extras: {
152-
whileConfig: {
153-
conditionField: "quality",
154-
conditionOperator: "less_than",
155-
conditionValue: "999",
156-
},
157-
},
131+
conditionField: "quality",
132+
conditionOperator: "less_than",
133+
conditionValue: "999",
158134
})
159135
.addTask(RefineTask)
160136
.endWhile();
@@ -177,13 +153,9 @@ describe("WhileTask with serialized whileConfig", () => {
177153
.while({
178154
maxIterations: 3,
179155
chainIterations: true,
180-
extras: {
181-
whileConfig: {
182-
conditionField: "",
183-
conditionOperator: "is_not_empty",
184-
conditionValue: "",
185-
},
186-
},
156+
conditionField: "",
157+
conditionOperator: "is_not_empty",
158+
conditionValue: "",
187159
})
188160
.addTask(RefineTask)
189161
.endWhile();
@@ -203,13 +175,6 @@ describe("WhileTask with serialized whileConfig", () => {
203175
condition: (output: { quality: number }, iteration: number) =>
204176
output.quality < 0.5 && iteration < 10,
205177
maxIterations: 20,
206-
extras: {
207-
whileConfig: {
208-
conditionField: "quality",
209-
conditionOperator: "less_than",
210-
conditionValue: "999", // would loop forever without maxIterations
211-
},
212-
},
213178
})
214179
.addTask(RefineTask)
215180
.endWhile();

0 commit comments

Comments
 (0)