@codemation/core-nodes 0.10.1 → 0.12.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/index.cjs +273 -108
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +212 -71
  5. package/dist/index.d.ts +213 -72
  6. package/dist/index.js +273 -105
  7. package/dist/index.js.map +1 -1
  8. package/dist/metadata.json +1 -1
  9. package/package.json +3 -2
  10. package/src/chatModels/CodemationChatModelConfig.ts +9 -21
  11. package/src/chatModels/CodemationChatModelFactory.ts +12 -9
  12. package/src/chatModels/OpenAIChatModelFactory.ts +3 -2
  13. package/src/http/HttpBodyBuilder.ts +9 -0
  14. package/src/http/httpRequest.types.ts +10 -1
  15. package/src/index.ts +1 -1
  16. package/src/nodes/AIAgentConfig.ts +28 -0
  17. package/src/nodes/AIAgentNode.ts +84 -17
  18. package/src/nodes/AgentBinaryContentFactory.ts +74 -0
  19. package/src/nodes/CallbackNodeFactory.ts +9 -6
  20. package/src/nodes/CronTriggerFactory.ts +6 -2
  21. package/src/nodes/DeferredMetaToolStrategy.ts +8 -2
  22. package/src/nodes/ManualTriggerFactory.ts +15 -11
  23. package/src/nodes/WebhookTriggerFactory.ts +9 -2
  24. package/src/nodes/aggregate.ts +9 -2
  25. package/src/nodes/assertion.ts +3 -0
  26. package/src/nodes/filter.ts +9 -2
  27. package/src/nodes/httpRequest.ts +7 -2
  28. package/src/nodes/if.ts +9 -2
  29. package/src/nodes/isTestRun.ts +6 -2
  30. package/src/nodes/mapData.ts +4 -2
  31. package/src/nodes/merge.ts +9 -2
  32. package/src/nodes/noOp.ts +9 -2
  33. package/src/nodes/nodeOptions.types.ts +12 -0
  34. package/src/nodes/split.ts +9 -2
  35. package/src/nodes/subWorkflow.ts +9 -2
  36. package/src/nodes/switch.ts +7 -1
  37. package/src/nodes/wait.ts +9 -2
  38. package/src/workflowAuthoring/WorkflowChatModelFactory.types.ts +8 -2
  39. package/src/chatModels/ManagedModelFetcher.ts +0 -23
@@ -1,5 +1,4 @@
1
1
  import type { ToolSet } from "ai";
2
- import { jsonSchema } from "ai";
3
2
  import { z } from "zod";
4
3
  import type { BM25Index } from "./BM25Index";
5
4
  import type {
@@ -37,6 +36,12 @@ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
37
36
  private mcpEntries: McpToolEntry[] = [];
38
37
  private toolsByServerId = new Map<string, Map<string, ToolSet[string]>>();
39
38
  private foundToolIds = new Set<string>();
39
+ /**
40
+ * `jsonSchema` from the `ai` SDK, loaded lazily in {@link initialize} so the SDK
41
+ * (~28MB RSS) stays off the boot path. `initialize` always runs before the sync
42
+ * `getToolsForTurn` → `buildFindToolsDefinition` path, so this is set before use.
43
+ */
44
+ private jsonSchema!: typeof import("ai").jsonSchema;
40
45
 
41
46
  constructor(
42
47
  private readonly bm25: BM25Index,
@@ -44,6 +49,7 @@ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
44
49
  ) {}
45
50
 
46
51
  async initialize(input: ToolLoadingStrategyInitInput): Promise<void> {
52
+ this.jsonSchema = (await import("ai")).jsonSchema;
47
53
  this.nodeBackedTools = { ...input.nodeBackedTools };
48
54
 
49
55
  const pinnedIds = input.pinnedMcpTools ?? [];
@@ -194,7 +200,7 @@ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
194
200
  "After this call, the tools listed in the result will be callable on your very next turn. " +
195
201
  "Use this when you need a capability not visible in your current tool list. " +
196
202
  "Do not attempt to call a tool name you have not seen yet — use find_tools to discover it first.",
197
- inputSchema: jsonSchema(inputSchemaRecord),
203
+ inputSchema: this.jsonSchema(inputSchemaRecord),
198
204
  } as unknown as ToolSet[string];
199
205
  }
200
206
  }
@@ -3,6 +3,7 @@ import type { Items, NodeInspectorSummaryRow, TriggerNodeConfig, TypeToken } fro
3
3
  import { ItemsInputNormalizer } from "@codemation/core";
4
4
 
5
5
  import { ManualTriggerNode } from "./ManualTriggerNode";
6
+ import type { NodeBaseOptions } from "./nodeOptions.types";
6
7
 
7
8
  type ManualTriggerDefaultValue<TOutputJson> = Items<TOutputJson> | ReadonlyArray<TOutputJson> | TOutputJson;
8
9
 
@@ -13,18 +14,28 @@ export class ManualTrigger<TOutputJson = unknown> implements TriggerNodeConfig<T
13
14
  readonly icon = "lucide:play" as const;
14
15
  readonly defaultItems?: Items<TOutputJson>;
15
16
  readonly id?: string;
17
+ readonly description?: string;
16
18
  /** Manual runs often emit an empty batch; still schedule downstream by default. */
17
19
  readonly continueWhenEmptyOutput = true as const;
18
20
 
19
- constructor(name?: string, id?: string);
20
- constructor(name: string, defaultItems: ManualTriggerDefaultValue<TOutputJson>, id?: string);
21
+ constructor(name?: string, idOrOptions?: string | NodeBaseOptions);
22
+ constructor(
23
+ name: string,
24
+ defaultItems: ManualTriggerDefaultValue<TOutputJson> | undefined,
25
+ idOrOptions?: string | NodeBaseOptions,
26
+ );
21
27
  constructor(
22
28
  public readonly name: string = "Manual trigger",
23
29
  defaultItemsOrId?: ManualTriggerDefaultValue<TOutputJson> | string,
24
- id?: string,
30
+ idOrOptions?: string | NodeBaseOptions,
25
31
  ) {
32
+ // Position 2 keeps its existing string-vs-object meaning (string→id, object/array→default items).
33
+ // Options (id/description) live in the trailing slot, or position 2 when it's a bare string id.
26
34
  this.defaultItems = ManualTrigger.resolveDefaultItems(defaultItemsOrId);
27
- this.id = ManualTrigger.resolveId(defaultItemsOrId, id);
35
+ const trailing = idOrOptions ?? (typeof defaultItemsOrId === "string" ? defaultItemsOrId : undefined);
36
+ const options = typeof trailing === "string" ? { id: trailing } : (trailing ?? {});
37
+ this.id = options.id;
38
+ this.description = options.description;
28
39
  }
29
40
 
30
41
  private static resolveDefaultItems<TOutputJson>(
@@ -36,13 +47,6 @@ export class ManualTrigger<TOutputJson = unknown> implements TriggerNodeConfig<T
36
47
  return this.itemsInputNormalizer.normalize(value) as Items<TOutputJson>;
37
48
  }
38
49
 
39
- private static resolveId<TOutputJson>(
40
- value: ManualTriggerDefaultValue<TOutputJson> | string | undefined,
41
- id: string | undefined,
42
- ): string | undefined {
43
- return typeof value === "string" ? value : id;
44
- }
45
-
46
50
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
47
51
  const rows: NodeInspectorSummaryRow[] = [{ label: "Trigger", value: "manual" }];
48
52
  if (this.defaultItems && this.defaultItems.length > 0) {
@@ -8,6 +8,7 @@ import type {
8
8
  } from "@codemation/core";
9
9
  import type { ZodType } from "zod";
10
10
  import { WebhookTriggerNode } from "./webhookTriggerNode";
11
+ import type { NodeBaseOptions } from "./nodeOptions.types";
11
12
 
12
13
  type WebhookInputSchema = ZodType<any, any, any>;
13
14
  type WebhookTriggerHandler<TConfig extends WebhookTrigger<any> = WebhookTrigger<any>> = (
@@ -21,6 +22,8 @@ export class WebhookTrigger<
21
22
  readonly kind = "trigger" as const;
22
23
  readonly type: TypeToken<unknown> = WebhookTriggerNode;
23
24
  readonly icon = "lucide:globe";
25
+ readonly id?: string;
26
+ readonly description?: string;
24
27
 
25
28
  constructor(
26
29
  public readonly name: string,
@@ -32,8 +35,12 @@ export class WebhookTrigger<
32
35
  public readonly handler: WebhookTriggerHandler<
33
36
  WebhookTrigger<TSchema>
34
37
  > = WebhookTrigger.defaultHandler as WebhookTriggerHandler<WebhookTrigger<TSchema>>,
35
- public readonly id?: string,
36
- ) {}
38
+ idOrOptions?: string | NodeBaseOptions,
39
+ ) {
40
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
41
+ this.id = options?.id;
42
+ this.description = options?.description;
43
+ }
37
44
 
38
45
  get endpointKey(): string {
39
46
  return this.args.endpointKey;
@@ -6,6 +6,7 @@ import type {
6
6
  TypeToken,
7
7
  } from "@codemation/core";
8
8
  import { AggregateNode } from "./AggregateNode";
9
+ import type { NodeBaseOptions } from "./nodeOptions.types";
9
10
 
10
11
  export class Aggregate<TIn = unknown, TOut = unknown> implements RunnableNodeConfig<TIn, TOut> {
11
12
  readonly kind = "node" as const;
@@ -13,6 +14,8 @@ export class Aggregate<TIn = unknown, TOut = unknown> implements RunnableNodeCon
13
14
  readonly execution = { hint: "local" } as const;
14
15
  readonly keepBinaries = true as const;
15
16
  readonly icon = "builtin:aggregate-rows" as const;
17
+ readonly id?: string;
18
+ readonly description?: string;
16
19
 
17
20
  constructor(
18
21
  public readonly name: string,
@@ -20,8 +23,12 @@ export class Aggregate<TIn = unknown, TOut = unknown> implements RunnableNodeCon
20
23
  items: Items<TIn>,
21
24
  ctx: NodeExecutionContext<Aggregate<TIn, TOut>>,
22
25
  ) => TOut | Promise<TOut>,
23
- public readonly id?: string,
24
- ) {}
26
+ idOrOptions?: string | NodeBaseOptions,
27
+ ) {
28
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
29
+ this.id = options?.id;
30
+ this.description = options?.description;
31
+ }
25
32
 
26
33
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
27
34
  const fnName = this.aggregate.name;
@@ -13,6 +13,7 @@ export interface AssertionOptions<TInputJson> {
13
13
  readonly name?: string;
14
14
  readonly id?: string;
15
15
  readonly icon?: string;
16
+ readonly description?: string;
16
17
  /**
17
18
  * Author callback. Returns one or more {@link AssertionResult}s per input item. Each becomes
18
19
  * one emitted output item — useful for per-row reporting in the Tests tab. Return `[]` to
@@ -35,6 +36,7 @@ export class Assertion<TInputJson = unknown> implements RunnableNodeConfig<TInpu
35
36
  readonly icon: string;
36
37
  readonly name: string;
37
38
  readonly id?: string;
39
+ readonly description?: string;
38
40
  readonly emitsAssertions = true as const;
39
41
  readonly assertions: AssertionOptions<TInputJson>["assertions"];
40
42
 
@@ -42,6 +44,7 @@ export class Assertion<TInputJson = unknown> implements RunnableNodeConfig<TInpu
42
44
  this.name = options.name ?? "Assertion";
43
45
  this.id = options.id;
44
46
  this.icon = options.icon ?? "lucide:check-circle";
47
+ this.description = options.description;
45
48
  this.assertions = options.assertions;
46
49
  }
47
50
 
@@ -7,12 +7,15 @@ import type {
7
7
  TypeToken,
8
8
  } from "@codemation/core";
9
9
  import { FilterNode } from "./FilterNode";
10
+ import type { NodeBaseOptions } from "./nodeOptions.types";
10
11
 
11
12
  export class Filter<TIn = unknown> implements RunnableNodeConfig<TIn, TIn> {
12
13
  readonly kind = "node" as const;
13
14
  readonly type: TypeToken<unknown> = FilterNode;
14
15
  readonly execution = { hint: "local" } as const;
15
16
  readonly icon = "lucide:filter" as const;
17
+ readonly id?: string;
18
+ readonly description?: string;
16
19
 
17
20
  constructor(
18
21
  public readonly name: string,
@@ -22,8 +25,12 @@ export class Filter<TIn = unknown> implements RunnableNodeConfig<TIn, TIn> {
22
25
  items: Items<TIn>,
23
26
  ctx: NodeExecutionContext<Filter<TIn>>,
24
27
  ) => boolean,
25
- public readonly id?: string,
26
- ) {}
28
+ idOrOptions?: string | NodeBaseOptions,
29
+ ) {
30
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
31
+ this.id = options?.id;
32
+ this.description = options?.description;
33
+ }
27
34
 
28
35
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
29
36
  const fnName = this.predicate.name;
@@ -62,6 +62,7 @@ export class HttpRequest<
62
62
  readonly type: TypeToken<unknown> = HttpRequestNode;
63
63
  readonly execution = { hint: "local" } as const;
64
64
  readonly icon = "lucide:globe" as const;
65
+ readonly description?: string;
65
66
 
66
67
  constructor(
67
68
  public readonly name: string,
@@ -79,7 +80,7 @@ export class HttpRequest<
79
80
  headers?: Readonly<Record<string, string>>;
80
81
  /** Query parameters to append to the URL. */
81
82
  query?: Readonly<Record<string, string>>;
82
- /** Request body specification. For canvas use, pass a JSON string in `body.data`. */
83
+ /** Request body specification. For `kind:"json"`, pass the object directly in `body.data` — it is JSON-encoded exactly once, so never a pre-stringified string. */
83
84
  body?: HttpBodySpec;
84
85
  /**
85
86
  * Credential slot.
@@ -136,9 +137,13 @@ export class HttpRequest<
136
137
  */
137
138
  allowedOutboundHosts?: ReadonlyArray<string>;
138
139
  id?: string;
140
+ /** Plain-language explanation surfaced in the node sidebar. */
141
+ description?: string;
139
142
  }> = {},
140
143
  public readonly retryPolicy: RetryPolicySpec = RetryPolicy.defaultForHttp,
141
- ) {}
144
+ ) {
145
+ this.description = args.description;
146
+ }
142
147
 
143
148
  get id(): string | undefined {
144
149
  return this.args.id;
package/src/nodes/if.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  TypeToken,
8
8
  } from "@codemation/core";
9
9
  import { IfNode } from "./IfNode";
10
+ import type { NodeBaseOptions } from "./nodeOptions.types";
10
11
 
11
12
  export class If<TInputJson = unknown> implements RunnableNodeConfig<TInputJson, TInputJson> {
12
13
  readonly kind = "node" as const;
@@ -14,6 +15,8 @@ export class If<TInputJson = unknown> implements RunnableNodeConfig<TInputJson,
14
15
  readonly execution = { hint: "local" } as const;
15
16
  readonly icon = "lucide:split@rot=90" as const;
16
17
  readonly declaredOutputPorts = ["true", "false"] as const;
18
+ readonly id?: string;
19
+ readonly description?: string;
17
20
  constructor(
18
21
  public readonly name: string,
19
22
  public readonly predicate: (
@@ -22,8 +25,12 @@ export class If<TInputJson = unknown> implements RunnableNodeConfig<TInputJson,
22
25
  items: Items<TInputJson>,
23
26
  ctx: NodeExecutionContext<If<TInputJson>>,
24
27
  ) => boolean,
25
- public readonly id?: string,
26
- ) {}
28
+ idOrOptions?: string | NodeBaseOptions,
29
+ ) {
30
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
31
+ this.id = options?.id;
32
+ this.description = options?.description;
33
+ }
27
34
 
28
35
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
29
36
  const fnName = this.predicate.name;
@@ -1,6 +1,7 @@
1
1
  import type { RunnableNodeConfig, TypeToken } from "@codemation/core";
2
2
 
3
3
  import { IsTestRunNode } from "./IsTestRunNode";
4
+ import type { NodeBaseOptions } from "./nodeOptions.types";
4
5
 
5
6
  /**
6
7
  * Branches per-item on whether the current run is a test run. Output ports: `true`, `false`.
@@ -14,10 +15,13 @@ export class IsTestRun<TInputJson = unknown> implements RunnableNodeConfig<TInpu
14
15
  readonly declaredOutputPorts = ["true", "false"] as const;
15
16
  readonly name: string;
16
17
  readonly id?: string;
18
+ readonly description?: string;
17
19
 
18
- constructor(name: string = "Is test run?", id?: string) {
20
+ constructor(name: string = "Is test run?", idOrOptions?: string | NodeBaseOptions) {
19
21
  this.name = name;
20
- this.id = id;
22
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
23
+ this.id = options?.id;
24
+ this.description = options?.description;
21
25
  }
22
26
  }
23
27
 
@@ -6,9 +6,9 @@ import type {
6
6
  TypeToken,
7
7
  } from "@codemation/core";
8
8
  import { MapDataNode } from "./MapDataNode";
9
+ import type { NodeBaseOptions } from "./nodeOptions.types";
9
10
 
10
- export interface MapDataOptions {
11
- readonly id?: string;
11
+ export interface MapDataOptions extends NodeBaseOptions {
12
12
  readonly keepBinaries?: boolean;
13
13
  }
14
14
 
@@ -23,6 +23,7 @@ export class MapData<TInputJson = unknown, TOutputJson = unknown> implements Run
23
23
  readonly continueWhenEmptyOutput = true as const;
24
24
  readonly icon = "lucide:square-pen" as const;
25
25
  readonly keepBinaries: boolean;
26
+ readonly description?: string;
26
27
 
27
28
  constructor(
28
29
  public readonly name: string,
@@ -33,6 +34,7 @@ export class MapData<TInputJson = unknown, TOutputJson = unknown> implements Run
33
34
  private readonly options: MapDataOptions = {},
34
35
  ) {
35
36
  this.keepBinaries = options.keepBinaries ?? true;
37
+ this.description = options.description;
36
38
  }
37
39
 
38
40
  get id(): string | undefined {
@@ -1,5 +1,6 @@
1
1
  import type { InputPortKey, NodeInspectorSummaryRow, RunnableNodeConfig, TypeToken } from "@codemation/core";
2
2
  import { MergeNode } from "./MergeNode";
3
+ import type { NodeBaseOptions } from "./nodeOptions.types";
3
4
 
4
5
  export type MergeMode = "passThrough" | "append" | "mergeByPosition";
5
6
 
@@ -10,6 +11,8 @@ export class Merge<TInputJson = unknown, TOutputJson = TInputJson> implements Ru
10
11
  readonly kind = "node" as const;
11
12
  readonly type: TypeToken<unknown> = MergeNode;
12
13
  readonly icon = "lucide:merge@rot=90" as const;
14
+ readonly id?: string;
15
+ readonly description?: string;
13
16
 
14
17
  constructor(
15
18
  public readonly name: string,
@@ -21,8 +24,12 @@ export class Merge<TInputJson = unknown, TOutputJson = TInputJson> implements Ru
21
24
  */
22
25
  prefer?: ReadonlyArray<InputPortKey>;
23
26
  }> = { mode: "passThrough" },
24
- public readonly id?: string,
25
- ) {}
27
+ idOrOptions?: string | NodeBaseOptions,
28
+ ) {
29
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
30
+ this.id = options?.id;
31
+ this.description = options?.description;
32
+ }
26
33
 
27
34
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
28
35
  const rows: NodeInspectorSummaryRow[] = [{ label: "Mode", value: this.cfg.mode }];
package/src/nodes/noOp.ts CHANGED
@@ -1,16 +1,23 @@
1
1
  import type { RunnableNodeConfig, TypeToken } from "@codemation/core";
2
2
  import { NoOpNode } from "./NoOpNode";
3
+ import type { NodeBaseOptions } from "./nodeOptions.types";
3
4
 
4
5
  export class NoOp<TItemJson = unknown> implements RunnableNodeConfig<TItemJson, TItemJson> {
5
6
  readonly kind = "node" as const;
6
7
  readonly type: TypeToken<unknown> = NoOpNode;
7
8
  readonly execution = { hint: "local" } as const;
8
9
  readonly icon = "lucide:circle-dashed" as const;
10
+ readonly id?: string;
11
+ readonly description?: string;
9
12
 
10
13
  constructor(
11
14
  public readonly name: string = "NoOp",
12
- public readonly id?: string,
13
- ) {}
15
+ idOrOptions?: string | NodeBaseOptions,
16
+ ) {
17
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
18
+ this.id = options?.id;
19
+ this.description = options?.description;
20
+ }
14
21
  }
15
22
 
16
23
  export { NoOpNode } from "./NoOpNode";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Options shared by every authorable built-in node: a stable `id` and a plain-language
3
+ * `description` (the non-technical "what does this node do" line surfaced in the node sidebar).
4
+ *
5
+ * `description` is a first-class config option — passed inline in the node's options, exactly like
6
+ * `id` — and is threaded onto the config instance so it flows into the persisted workflow snapshot
7
+ * the host / canvas mappers read. Node-specific option types extend this.
8
+ */
9
+ export interface NodeBaseOptions {
10
+ readonly id?: string;
11
+ readonly description?: string;
12
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  TypeToken,
7
7
  } from "@codemation/core";
8
8
  import { SplitNode } from "./SplitNode";
9
+ import type { NodeBaseOptions } from "./nodeOptions.types";
9
10
 
10
11
  export class Split<TIn = unknown, TElem = unknown> implements RunnableNodeConfig<TIn, TElem> {
11
12
  readonly kind = "node" as const;
@@ -18,12 +19,18 @@ export class Split<TIn = unknown, TElem = unknown> implements RunnableNodeConfig
18
19
  */
19
20
  readonly continueWhenEmptyOutput = true as const;
20
21
  readonly icon = "builtin:split-rows" as const;
22
+ readonly id?: string;
23
+ readonly description?: string;
21
24
 
22
25
  constructor(
23
26
  public readonly name: string,
24
27
  public readonly getElements: (item: Item<TIn>, ctx: NodeExecutionContext<Split<TIn, TElem>>) => readonly TElem[],
25
- public readonly id?: string,
26
- ) {}
28
+ idOrOptions?: string | NodeBaseOptions,
29
+ ) {
30
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
31
+ this.id = options?.id;
32
+ this.description = options?.description;
33
+ }
27
34
 
28
35
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
29
36
  const fnName = this.getElements.name;
@@ -6,6 +6,7 @@ import type {
6
6
  UpstreamRefPlaceholder,
7
7
  } from "@codemation/core";
8
8
  import { SubWorkflowNode } from "./SubWorkflowNode";
9
+ import type { NodeBaseOptions } from "./nodeOptions.types";
9
10
 
10
11
  export class SubWorkflow<TInputJson = unknown, TOutputJson = unknown> implements RunnableNodeConfig<
11
12
  TInputJson,
@@ -14,13 +15,19 @@ export class SubWorkflow<TInputJson = unknown, TOutputJson = unknown> implements
14
15
  readonly kind = "node" as const;
15
16
  readonly type: TypeToken<unknown> = SubWorkflowNode;
16
17
  readonly icon = "lucide:workflow";
18
+ readonly id?: string;
19
+ readonly description?: string;
17
20
  constructor(
18
21
  public readonly name: string,
19
22
  public readonly workflowId: string,
20
23
  public upstreamRefs?: Array<{ nodeId: NodeId } | UpstreamRefPlaceholder>,
21
24
  public readonly startAt?: NodeId,
22
- public readonly id?: string,
23
- ) {}
25
+ idOrOptions?: string | NodeBaseOptions,
26
+ ) {
27
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
28
+ this.id = options?.id;
29
+ this.description = options?.description;
30
+ }
24
31
 
25
32
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
26
33
  const rows: NodeInspectorSummaryRow[] = [{ label: "Workflow", value: this.workflowId }];
@@ -7,6 +7,7 @@ import type {
7
7
  TypeToken,
8
8
  } from "@codemation/core";
9
9
  import { SwitchNode } from "./SwitchNode";
10
+ import type { NodeBaseOptions } from "./nodeOptions.types";
10
11
 
11
12
  export type SwitchCaseKeyResolver<TInputJson = unknown> = (
12
13
  item: Item<TInputJson>,
@@ -21,6 +22,8 @@ export class Switch<TInputJson = unknown> implements RunnableNodeConfig<TInputJs
21
22
  readonly execution = { hint: "local" } as const;
22
23
  readonly icon = "lucide:git-branch-plus" as const;
23
24
  readonly declaredOutputPorts: ReadonlyArray<string>;
25
+ readonly id?: string;
26
+ readonly description?: string;
24
27
 
25
28
  constructor(
26
29
  public readonly name: string,
@@ -29,9 +32,12 @@ export class Switch<TInputJson = unknown> implements RunnableNodeConfig<TInputJs
29
32
  defaultCase: string;
30
33
  resolveCaseKey: SwitchCaseKeyResolver<TInputJson>;
31
34
  }>,
32
- public readonly id?: string,
35
+ idOrOptions?: string | NodeBaseOptions,
33
36
  ) {
34
37
  this.declaredOutputPorts = [...new Set([...cfg.cases, cfg.defaultCase])].sort();
38
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
39
+ this.id = options?.id;
40
+ this.description = options?.description;
35
41
  }
36
42
 
37
43
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
package/src/nodes/wait.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { NodeInspectorSummaryRow, RunnableNodeConfig, TypeToken } from "@codemation/core";
2
2
 
3
3
  import { WaitNode } from "./WaitNode";
4
+ import type { NodeBaseOptions } from "./nodeOptions.types";
4
5
 
5
6
  export class Wait<TItemJson = unknown> implements RunnableNodeConfig<TItemJson, TItemJson> {
6
7
  readonly kind = "node" as const;
@@ -9,12 +10,18 @@ export class Wait<TItemJson = unknown> implements RunnableNodeConfig<TItemJson,
9
10
  /** Pass-through empty batches should still advance to downstream nodes. */
10
11
  readonly continueWhenEmptyOutput = true as const;
11
12
  readonly icon = "lucide:hourglass" as const;
13
+ readonly id?: string;
14
+ readonly description?: string;
12
15
 
13
16
  constructor(
14
17
  public readonly name: string,
15
18
  public readonly milliseconds: number,
16
- public readonly id?: string,
17
- ) {}
19
+ idOrOptions?: string | NodeBaseOptions,
20
+ ) {
21
+ const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
22
+ this.id = options?.id;
23
+ this.description = options?.description;
24
+ }
18
25
 
19
26
  inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
20
27
  const seconds = this.milliseconds / 1000;
@@ -1,6 +1,8 @@
1
1
  import type { ChatModelConfig } from "@codemation/core";
2
2
  import { OpenAIChatModelConfig } from "../chatModels/openAiChatModelConfig";
3
- import { CodemationChatModelConfig } from "../chatModels/CodemationChatModelConfig";
3
+ import { CodemationChatModelConfig, type ManagedComplexity } from "../chatModels/CodemationChatModelConfig";
4
+
5
+ const VALID_COMPLEXITY: ReadonlySet<string> = new Set(["low", "medium", "high", "xhigh"]);
4
6
 
5
7
  export class WorkflowChatModelFactory {
6
8
  static create(model: string | ChatModelConfig): ChatModelConfig {
@@ -9,7 +11,11 @@ export class WorkflowChatModelFactory {
9
11
  }
10
12
  const [provider, resolvedModel] = model.includes(":") ? model.split(":", 2) : ["openai", model];
11
13
  if (provider === "codemation-managed") {
12
- return new CodemationChatModelConfig("Codemation Managed", resolvedModel ?? "");
14
+ const complexity = resolvedModel ?? "medium";
15
+ if (!VALID_COMPLEXITY.has(complexity)) {
16
+ throw new Error(`Invalid managed complexity "${complexity}". Must be one of: low, medium, high, xhigh.`);
17
+ }
18
+ return new CodemationChatModelConfig("Codemation Managed", complexity as ManagedComplexity);
13
19
  }
14
20
  if (provider !== "openai") {
15
21
  throw new Error(`Unsupported workflow().agent() model provider "${provider}".`);
@@ -1,23 +0,0 @@
1
- import type { ManagedModelDto } from "./CodemationChatModelConfig";
2
-
3
- /**
4
- * Fetches the active platform-managed model allowlist from the CP.
5
- * Reads CONTROL_PLANE_URL from the workspace process env.
6
- * Returns an empty array if the env var is absent or the fetch fails.
7
- * Cache the result per session — the allowlist changes infrequently.
8
- */
9
- export class ManagedModelFetcher {
10
- async fetch(): Promise<ManagedModelDto[]> {
11
- // eslint-disable-next-line no-restricted-properties -- CONTROL_PLANE_URL is injected by the provisioner; this class is the justified boundary.
12
- const cpUrl = process.env["CONTROL_PLANE_URL"];
13
- if (!cpUrl) return [];
14
-
15
- try {
16
- const res = await globalThis.fetch(`${cpUrl}/api/llm/managed-models`);
17
- if (!res.ok) return [];
18
- return (await res.json()) as ManagedModelDto[];
19
- } catch {
20
- return [];
21
- }
22
- }
23
- }