@memberjunction/ai-agent-manager 6.1.0-edge.0 → 6.1.0-edge.2
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +103 -0
- package/dist/agent-spec-sync.d.ts.map +1 -1
- package/dist/agent-spec-sync.js +13 -7
- package/dist/agent-spec-sync.js.map +1 -1
- package/dist/agents/architect-agent.d.ts.map +1 -1
- package/dist/agents/architect-agent.js +15 -2
- package/dist/agents/architect-agent.js.map +1 -1
- package/dist/flow-step-validation.d.ts +29 -0
- package/dist/flow-step-validation.d.ts.map +1 -0
- package/dist/flow-step-validation.js +102 -0
- package/dist/flow-step-validation.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/workflow-agent-writer.d.ts +48 -0
- package/dist/workflow-agent-writer.d.ts.map +1 -0
- package/dist/workflow-agent-writer.js +97 -0
- package/dist/workflow-agent-writer.js.map +1 -0
- package/package.json +9 -9
- package/src/__tests__/flow-step-validation.test.ts +161 -0
- package/src/agent-spec-sync.ts +18 -10
- package/src/agents/architect-agent.ts +16 -2
- package/src/flow-step-validation.ts +137 -0
- package/src/index.ts +2 -0
- package/src/workflow-agent-writer.ts +117 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** True when this step repeats a body rather than being one. */
|
|
2
|
+
export function IsLoopStep(step) {
|
|
3
|
+
return step.StepType === 'ForEach' || step.StepType === 'While';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Which field carries the body's id, per body type.
|
|
7
|
+
*
|
|
8
|
+
* The body's id lives in the SAME field a plain step of that type would use — a `ForEach` whose body
|
|
9
|
+
* is an Action still puts it in `ActionID`. Introducing a parallel `LoopBodyActionID` would have been
|
|
10
|
+
* a second place an action id can live, and the two would drift.
|
|
11
|
+
*/
|
|
12
|
+
const BODY_ID_FIELD = {
|
|
13
|
+
Action: 'ActionID',
|
|
14
|
+
Prompt: 'PromptID',
|
|
15
|
+
'Sub-Agent': 'SubAgentID',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Checks one `ForEach` / `While` step, returning every problem rather than the first.
|
|
19
|
+
*
|
|
20
|
+
* Returns messages rather than throwing, matching how the Architect reports the rest of its
|
|
21
|
+
* validation — one pass gives the model everything it has to fix, instead of a fix-and-retry loop
|
|
22
|
+
* that surfaces one error per round trip.
|
|
23
|
+
*/
|
|
24
|
+
export function ValidateLoopStep(step, index) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
if (!IsLoopStep(step))
|
|
27
|
+
return errors;
|
|
28
|
+
const where = `${step.StepType} step "${step.Name}" (index ${index})`;
|
|
29
|
+
validateLoopBody(step, where, errors);
|
|
30
|
+
const config = parseLoopConfiguration(step, where, errors);
|
|
31
|
+
if (config)
|
|
32
|
+
validateLoopBounds(step, config, where, errors);
|
|
33
|
+
return errors;
|
|
34
|
+
}
|
|
35
|
+
/** The loop must name what it repeats, and that thing must exist. */
|
|
36
|
+
function validateLoopBody(step, where, errors) {
|
|
37
|
+
if (!step.LoopBodyType) {
|
|
38
|
+
errors.push(`❌ ${where} must have LoopBodyType — one of "Action", "Prompt" or "Sub-Agent" — naming what runs on each pass`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const field = BODY_ID_FIELD[step.LoopBodyType];
|
|
42
|
+
if (step[field])
|
|
43
|
+
return;
|
|
44
|
+
// Two legitimate reasons the id is still empty:
|
|
45
|
+
// - a Sub-Agent body that AgentSpecSync will link by name once the sub-agent is created, exactly
|
|
46
|
+
// as it already does for a plain Sub-Agent step;
|
|
47
|
+
// - a Prompt body supplied inline as PromptText, which becomes an AIPrompt on save.
|
|
48
|
+
const linkedLater = step.LoopBodyType === 'Sub-Agent';
|
|
49
|
+
const inlinePrompt = step.LoopBodyType === 'Prompt' && !!step.PromptText?.trim();
|
|
50
|
+
if (linkedLater || inlinePrompt)
|
|
51
|
+
return;
|
|
52
|
+
errors.push(`❌ ${where} has LoopBodyType "${step.LoopBodyType}" but no ${field} to run`);
|
|
53
|
+
}
|
|
54
|
+
/** ForEach needs something to iterate; While needs something to test; both need a name for the item. */
|
|
55
|
+
function validateLoopBounds(step, config, where, errors) {
|
|
56
|
+
if (step.StepType === 'ForEach' && !config['collectionPath']) {
|
|
57
|
+
errors.push(`❌ ${where} must set Configuration.collectionPath — the payload path holding the items to iterate`);
|
|
58
|
+
}
|
|
59
|
+
if (step.StepType === 'While' && !config['condition']) {
|
|
60
|
+
errors.push(`❌ ${where} must set Configuration.condition — the expression checked before each pass`);
|
|
61
|
+
}
|
|
62
|
+
if (!config['itemVariable']) {
|
|
63
|
+
errors.push(`❌ ${where} must set Configuration.itemVariable — the name the body refers to the current item by`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reads a loop's `Configuration`, reporting rather than throwing. Null when unusable.
|
|
68
|
+
*
|
|
69
|
+
* Accepts an object as well as a JSON string — the same latitude the action mappings already get,
|
|
70
|
+
* because a model that has just been shown an object literal in the prompt will send one.
|
|
71
|
+
*/
|
|
72
|
+
function parseLoopConfiguration(step, where, errors) {
|
|
73
|
+
const raw = step.Configuration;
|
|
74
|
+
if (raw == null || raw === '') {
|
|
75
|
+
errors.push(`❌ ${where} must have a Configuration object describing the loop's bounds`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (typeof raw === 'object') {
|
|
79
|
+
if (Array.isArray(raw)) {
|
|
80
|
+
errors.push(`❌ ${where} has a Configuration that is an array rather than an object`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return raw;
|
|
84
|
+
}
|
|
85
|
+
if (typeof raw !== 'string') {
|
|
86
|
+
errors.push(`❌ ${where} has a Configuration that is neither an object nor JSON text`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(raw);
|
|
91
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
92
|
+
errors.push(`❌ ${where} has a Configuration that is not a JSON object`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
errors.push(`❌ ${where} has invalid Configuration JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=flow-step-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-step-validation.js","sourceRoot":"","sources":["../src/flow-step-validation.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export * from './old/agent-definition.interface.js';
|
|
7
7
|
export * from './agent-spec-sync.js';
|
|
8
|
+
export * from './flow-step-validation.js';
|
|
8
9
|
export * from './agents/architect-agent.js';
|
|
9
10
|
export * from './agents/builder-agent.js';
|
|
10
11
|
export * from './agents/planning-designer-agent.js';
|
|
12
|
+
export * from './workflow-agent-writer.js';
|
|
11
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,kCAAkC,CAAC;AAEjD,cAAc,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,kCAAkC,CAAC;AAEjD,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kCAAkC,CAAC;AACjD,cAAc,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// Export all interfaces
|
|
7
7
|
export * from './old/agent-definition.interface.js';
|
|
8
8
|
export * from './agent-spec-sync.js';
|
|
9
|
+
export * from './flow-step-validation.js';
|
|
9
10
|
// Export agent implementations
|
|
10
11
|
export * from './agents/architect-agent.js';
|
|
11
12
|
export * from './agents/builder-agent.js';
|
|
12
13
|
export * from './agents/planning-designer-agent.js';
|
|
14
|
+
export * from './workflow-agent-writer.js';
|
|
13
15
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAwB;AACxB,cAAc,kCAAkC,CAAC;AAEjD,cAAc,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAwB;AACxB,cAAc,kCAAkC,CAAC;AAEjD,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AAEvC,+BAA+B;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kCAAkC,CAAC;AACjD,cAAc,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Persists a workflow's graph as a Flow agent, via `AgentSpecSync`.
|
|
3
|
+
*
|
|
4
|
+
* This is the host side of the seam `WorkflowSpecSync` declares. It lives here, in the agent-manager,
|
|
5
|
+
* because `AgentSpecSync` is the **one place that writes an agent** — it already owns atomic
|
|
6
|
+
* multi-entity writes and the mutation audit, and a second writer would be a second set of rules
|
|
7
|
+
* about what a valid agent record is.
|
|
8
|
+
*
|
|
9
|
+
* The conversion itself is `ConvertTaskGraphToAgentSpec`, shipped in Phase 4. That it is reusable
|
|
10
|
+
* here without modification is the practical payoff of the convergence: "save a runtime graph as a
|
|
11
|
+
* workflow" and "persist a workflow's graph" turn out to be the same operation, because after Phase 4
|
|
12
|
+
* they are the same model.
|
|
13
|
+
*
|
|
14
|
+
* @module @memberjunction/ai-agent-manager
|
|
15
|
+
*/
|
|
16
|
+
import { UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
17
|
+
import { type WorkflowSpec } from '@memberjunction/ai-core-plus';
|
|
18
|
+
/** ClassFactory key under which hosts resolve the writer. */
|
|
19
|
+
export declare const WORKFLOW_AGENT_WRITER_KEY = "WorkflowAgentWriter";
|
|
20
|
+
/** Base so hosts can resolve an implementation without importing this package directly. */
|
|
21
|
+
export declare abstract class WorkflowAgentWriterBase {
|
|
22
|
+
abstract PersistFlowAgent(spec: WorkflowSpec, context: {
|
|
23
|
+
ContextUser: UserInfo;
|
|
24
|
+
Provider: IMetadataProvider;
|
|
25
|
+
}): Promise<string>;
|
|
26
|
+
}
|
|
27
|
+
export declare class WorkflowAgentWriter extends WorkflowAgentWriterBase {
|
|
28
|
+
/**
|
|
29
|
+
* Converts the workflow's graph to a Flow `AgentSpec` and persists it.
|
|
30
|
+
*
|
|
31
|
+
* **Losses are logged, not swallowed.** The converter reports what it could not carry across —
|
|
32
|
+
* human steps, unresolvable agents, run-specific inputs. Dropping that silently would hand
|
|
33
|
+
* someone a workflow missing an approval they believed they had saved, and they would only find
|
|
34
|
+
* out by running it. The save still proceeds: a workflow that is 90% right and says so is more
|
|
35
|
+
* useful than a refusal, and the losses are surfaced to the caller by the operation above.
|
|
36
|
+
*/
|
|
37
|
+
PersistFlowAgent(spec: WorkflowSpec, context: {
|
|
38
|
+
ContextUser: UserInfo;
|
|
39
|
+
Provider: IMetadataProvider;
|
|
40
|
+
}): Promise<string>;
|
|
41
|
+
/** Resolves the Flow agent type, so the persisted agent is a Flow rather than a Loop. */
|
|
42
|
+
private resolveFlowAgentTypeID;
|
|
43
|
+
/** Name → ID for every agent, lowercased so a spec's human-entered name still resolves. */
|
|
44
|
+
private buildAgentNameIndex;
|
|
45
|
+
}
|
|
46
|
+
/** Prevents tree-shaking of the registered writer. */
|
|
47
|
+
export declare function LoadWorkflowAgentWriter(): void;
|
|
48
|
+
//# sourceMappingURL=workflow-agent-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-agent-writer.d.ts","sourceRoot":"","sources":["../src/workflow-agent-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAqB,QAAQ,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEtF,OAAO,EAGH,KAAK,YAAY,EACpB,MAAM,8BAA8B,CAAC;AAGtC,6DAA6D;AAC7D,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D,2FAA2F;AAC3F,8BAAsB,uBAAuB;aACzB,gBAAgB,CAC5B,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE;QAAE,WAAW,EAAE,QAAQ,CAAC;QAAC,QAAQ,EAAE,iBAAiB,CAAA;KAAE,GAChE,OAAO,CAAC,MAAM,CAAC;CACrB;AAED,qBACa,mBAAoB,SAAQ,uBAAuB;IAC5D;;;;;;;;OAQG;IACU,gBAAgB,CACzB,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE;QAAE,WAAW,EAAE,QAAQ,CAAC;QAAC,QAAQ,EAAE,iBAAiB,CAAA;KAAE,GAChE,OAAO,CAAC,MAAM,CAAC;IAyClB,yFAAyF;YAC3E,sBAAsB;IAUpC,2FAA2F;YAC7E,mBAAmB;CASpC;AAED,sDAAsD;AACtD,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview Persists a workflow's graph as a Flow agent, via `AgentSpecSync`.
|
|
9
|
+
*
|
|
10
|
+
* This is the host side of the seam `WorkflowSpecSync` declares. It lives here, in the agent-manager,
|
|
11
|
+
* because `AgentSpecSync` is the **one place that writes an agent** — it already owns atomic
|
|
12
|
+
* multi-entity writes and the mutation audit, and a second writer would be a second set of rules
|
|
13
|
+
* about what a valid agent record is.
|
|
14
|
+
*
|
|
15
|
+
* The conversion itself is `ConvertTaskGraphToAgentSpec`, shipped in Phase 4. That it is reusable
|
|
16
|
+
* here without modification is the practical payoff of the convergence: "save a runtime graph as a
|
|
17
|
+
* workflow" and "persist a workflow's graph" turn out to be the same operation, because after Phase 4
|
|
18
|
+
* they are the same model.
|
|
19
|
+
*
|
|
20
|
+
* @module @memberjunction/ai-agent-manager
|
|
21
|
+
*/
|
|
22
|
+
import { LogError, RunView } from '@memberjunction/core';
|
|
23
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
24
|
+
import { ConvertTaskGraphToAgentSpec, FormatSaveAsWorkflowLosses, } from '@memberjunction/ai-core-plus';
|
|
25
|
+
import { AgentSpecSync } from './agent-spec-sync.js';
|
|
26
|
+
/** ClassFactory key under which hosts resolve the writer. */
|
|
27
|
+
export const WORKFLOW_AGENT_WRITER_KEY = 'WorkflowAgentWriter';
|
|
28
|
+
/** Base so hosts can resolve an implementation without importing this package directly. */
|
|
29
|
+
export class WorkflowAgentWriterBase {
|
|
30
|
+
}
|
|
31
|
+
let WorkflowAgentWriter = class WorkflowAgentWriter extends WorkflowAgentWriterBase {
|
|
32
|
+
/**
|
|
33
|
+
* Converts the workflow's graph to a Flow `AgentSpec` and persists it.
|
|
34
|
+
*
|
|
35
|
+
* **Losses are logged, not swallowed.** The converter reports what it could not carry across —
|
|
36
|
+
* human steps, unresolvable agents, run-specific inputs. Dropping that silently would hand
|
|
37
|
+
* someone a workflow missing an approval they believed they had saved, and they would only find
|
|
38
|
+
* out by running it. The save still proceeds: a workflow that is 90% right and says so is more
|
|
39
|
+
* useful than a refusal, and the losses are surfaced to the caller by the operation above.
|
|
40
|
+
*/
|
|
41
|
+
async PersistFlowAgent(spec, context) {
|
|
42
|
+
const flowTypeID = await this.resolveFlowAgentTypeID(context);
|
|
43
|
+
const agentIDsByName = await this.buildAgentNameIndex(context);
|
|
44
|
+
let counter = 0;
|
|
45
|
+
const result = ConvertTaskGraphToAgentSpec(spec.graph, {
|
|
46
|
+
// Deterministic within one call, and unique because AgentSpecSync assigns real keys on
|
|
47
|
+
// insert — these only have to correlate steps to paths inside this payload.
|
|
48
|
+
AgentID: `workflow-${Date.now()}-${counter}`,
|
|
49
|
+
NextID: () => `wf-node-${++counter}`,
|
|
50
|
+
ResolveAgentID: (name) => agentIDsByName.get(name.trim().toLowerCase()) ?? null,
|
|
51
|
+
FlowAgentTypeID: flowTypeID,
|
|
52
|
+
Name: spec.name,
|
|
53
|
+
});
|
|
54
|
+
if (!result.Success || !result.Spec) {
|
|
55
|
+
throw new Error(result.ErrorMessage ?? 'The workflow graph could not be converted to an agent.');
|
|
56
|
+
}
|
|
57
|
+
if (result.Losses.length > 0) {
|
|
58
|
+
LogError(`[WorkflowAgentWriter] "${spec.name}" saved with losses:\n${FormatSaveAsWorkflowLosses(result.Losses)}`);
|
|
59
|
+
}
|
|
60
|
+
result.Spec.Description = spec.description ?? result.Spec.Description;
|
|
61
|
+
// A Draft or Paused workflow persists as a Disabled agent, so nothing can invoke it before
|
|
62
|
+
// its author has turned it on — the schedule side makes the same choice for the same reason.
|
|
63
|
+
// 'Disabled' and not 'Inactive': the latter is not a value `AIAgent.Status` accepts, so this
|
|
64
|
+
// line used to make every non-Active save fail its CHECK constraint.
|
|
65
|
+
result.Spec.Status = spec.status === 'Active' ? 'Active' : 'Disabled';
|
|
66
|
+
const sync = AgentSpecSync.FromRawSpec(result.Spec, context.ContextUser, context.Provider);
|
|
67
|
+
// AgentSpecSyncResult uses camelCase — it predates the PascalCase-public convention and is
|
|
68
|
+
// consumed by MCP tools, so it is left alone rather than renamed under this change.
|
|
69
|
+
const saved = await sync.SaveToDatabase();
|
|
70
|
+
if (!saved?.success) {
|
|
71
|
+
throw new Error(`Could not save the workflow's agent "${spec.name}".`);
|
|
72
|
+
}
|
|
73
|
+
return saved.agentId;
|
|
74
|
+
}
|
|
75
|
+
/** Resolves the Flow agent type, so the persisted agent is a Flow rather than a Loop. */
|
|
76
|
+
async resolveFlowAgentTypeID(context) {
|
|
77
|
+
const result = await RunView.FromMetadataProvider(context.Provider).RunView({ EntityName: 'MJ: AI Agent Types', ExtraFilter: `Name='Flow'`, Fields: ['ID'], ResultType: 'simple' }, context.ContextUser);
|
|
78
|
+
const id = result.Results?.[0]?.ID;
|
|
79
|
+
if (!id)
|
|
80
|
+
throw new Error("The 'Flow' agent type was not found — has the metadata seed been pushed?");
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
/** Name → ID for every agent, lowercased so a spec's human-entered name still resolves. */
|
|
84
|
+
async buildAgentNameIndex(context) {
|
|
85
|
+
const result = await RunView.FromMetadataProvider(context.Provider).RunView({ EntityName: 'MJ: AI Agents', Fields: ['ID', 'Name'], ResultType: 'simple' }, context.ContextUser);
|
|
86
|
+
return new Map((result.Results ?? []).map((a) => [a.Name.trim().toLowerCase(), a.ID]));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
WorkflowAgentWriter = __decorate([
|
|
90
|
+
RegisterClass(WorkflowAgentWriterBase, WORKFLOW_AGENT_WRITER_KEY)
|
|
91
|
+
], WorkflowAgentWriter);
|
|
92
|
+
export { WorkflowAgentWriter };
|
|
93
|
+
/** Prevents tree-shaking of the registered writer. */
|
|
94
|
+
export function LoadWorkflowAgentWriter() {
|
|
95
|
+
void WorkflowAgentWriter;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=workflow-agent-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-agent-writer.js","sourceRoot":"","sources":["../src/workflow-agent-writer.ts"],"names":[],"mappings":";;;;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAA+B,MAAM,sBAAsB,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EACH,2BAA2B,EAC3B,0BAA0B,GAE7B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,yBAAyB,GAAG,qBAAqB,CAAC;AAE/D,2FAA2F;AAC3F,MAAM,OAAgB,uBAAuB;CAK5C;AAGM,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,uBAAuB;IAC5D;;;;;;;;OAQG;IACI,KAAK,CAAC,gBAAgB,CACzB,IAAkB,EAClB,OAA+D;QAE/D,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC;QAC9D,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAE/D,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,MAAM,MAAM,GAAG,2BAA2B,CAAC,IAAI,CAAC,KAAK,EAAE;YACnD,uFAAuF;YACvF,4EAA4E;YAC5E,OAAO,EAAE,YAAY,IAAI,CAAC,GAAG,EAAE,IAAI,OAAO,EAAE;YAC5C,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE;YACpC,cAAc,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI;YAC/E,eAAe,EAAE,UAAU;YAC3B,IAAI,EAAE,IAAI,CAAC,IAAI;SAClB,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,YAAY,IAAI,wDAAwD,CAAC,CAAC;QACrG,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,QAAQ,CACJ,0BAA0B,IAAI,CAAC,IAAI,yBAAyB,0BAA0B,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAC1G,CAAC;QACN,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;QACtE,2FAA2F;QAC3F,6FAA6F;QAC7F,6FAA6F;QAC7F,qEAAqE;QACrE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC;QAEtE,MAAM,IAAI,GAAG,aAAa,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC3F,2FAA2F;QAC3F,oFAAoF;QACpF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,KAAK,CAAC,OAAO,CAAC;IACzB,CAAC;IAED,yFAAyF;IACjF,KAAK,CAAC,sBAAsB,CAAC,OAA+D;QAChG,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,oBAAoB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CACvE,EAAE,UAAU,EAAE,oBAAoB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,EACtG,OAAO,CAAC,WAAW,CACtB,CAAC;QACF,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACnC,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;QACrG,OAAO,EAAE,CAAC;IACd,CAAC;IAED,2FAA2F;IACnF,KAAK,CAAC,mBAAmB,CAC7B,OAA+D;QAE/D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,oBAAoB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CACvE,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,EAC7E,OAAO,CAAC,WAAW,CACtB,CAAC;QACF,OAAO,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3F,CAAC;CACJ,CAAA;AA3EY,mBAAmB;IAD/B,aAAa,CAAC,uBAAuB,EAAE,yBAAyB,CAAC;GACrD,mBAAmB,CA2E/B;;AAED,sDAAsD;AACtD,MAAM,UAAU,uBAAuB;IACnC,KAAK,mBAAmB,CAAC;AAC7B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ai-agent-manager",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "6.1.0-edge.
|
|
4
|
+
"version": "6.1.0-edge.2",
|
|
5
5
|
"description": "Core interfaces and types for the MemberJunction Agent Manager system",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
"author": "MemberJunction",
|
|
15
15
|
"license": "ISC",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@memberjunction/ai-agents": "6.1.0-edge.
|
|
18
|
-
"@memberjunction/ai-core-plus": "6.1.0-edge.
|
|
19
|
-
"@memberjunction/ai-engine-base": "6.1.0-edge.
|
|
20
|
-
"@memberjunction/aiengine": "6.1.0-edge.
|
|
21
|
-
"@memberjunction/core": "6.1.0-edge.
|
|
22
|
-
"@memberjunction/core-entities": "6.1.0-edge.
|
|
23
|
-
"@memberjunction/global": "6.1.0-edge.
|
|
24
|
-
"@memberjunction/templates": "6.1.0-edge.
|
|
17
|
+
"@memberjunction/ai-agents": "6.1.0-edge.2",
|
|
18
|
+
"@memberjunction/ai-core-plus": "6.1.0-edge.2",
|
|
19
|
+
"@memberjunction/ai-engine-base": "6.1.0-edge.2",
|
|
20
|
+
"@memberjunction/aiengine": "6.1.0-edge.2",
|
|
21
|
+
"@memberjunction/core": "6.1.0-edge.2",
|
|
22
|
+
"@memberjunction/core-entities": "6.1.0-edge.2",
|
|
23
|
+
"@memberjunction/global": "6.1.0-edge.2",
|
|
24
|
+
"@memberjunction/templates": "6.1.0-edge.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^24.10.11",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for loop-step validation.
|
|
3
|
+
*
|
|
4
|
+
* The assertion that matters most here is the one about a loop that saves cleanly and then does
|
|
5
|
+
* nothing: a `ForEach` with no `collectionPath` is structurally valid SQL, passes every other check,
|
|
6
|
+
* and at runtime iterates zero times — which reads as the agent declining to do the work rather than
|
|
7
|
+
* as a malformed step. That is precisely the class of error the Architect has to catch before save.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import type { AgentStep } from '@memberjunction/ai-core-plus';
|
|
11
|
+
import { IsLoopStep, ValidateLoopStep } from '../flow-step-validation';
|
|
12
|
+
|
|
13
|
+
const step = (over: Partial<AgentStep> = {}): AgentStep => ({
|
|
14
|
+
ID: '',
|
|
15
|
+
Name: 'Score each lead',
|
|
16
|
+
StepType: 'ForEach',
|
|
17
|
+
StartingStep: false,
|
|
18
|
+
LoopBodyType: 'Action',
|
|
19
|
+
ActionID: 'AC71E1DA-1111-2222-3333-444455556666',
|
|
20
|
+
Configuration: JSON.stringify({ type: 'ForEach', collectionPath: 'leads', itemVariable: 'lead' }),
|
|
21
|
+
...over,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('IsLoopStep', () => {
|
|
25
|
+
it('recognises exactly the two wrapping step types', () => {
|
|
26
|
+
expect(IsLoopStep({ StepType: 'ForEach' })).toBe(true);
|
|
27
|
+
expect(IsLoopStep({ StepType: 'While' })).toBe(true);
|
|
28
|
+
expect(IsLoopStep({ StepType: 'Action' })).toBe(false);
|
|
29
|
+
expect(IsLoopStep({ StepType: 'Prompt' })).toBe(false);
|
|
30
|
+
expect(IsLoopStep({ StepType: 'Sub-Agent' })).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('ValidateLoopStep — the happy paths', () => {
|
|
35
|
+
it('accepts a well-formed ForEach', () => {
|
|
36
|
+
expect(ValidateLoopStep(step(), 0)).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('accepts a well-formed While', () => {
|
|
40
|
+
const s = step({
|
|
41
|
+
StepType: 'While',
|
|
42
|
+
Name: 'Poll until done',
|
|
43
|
+
Configuration: JSON.stringify({ type: 'While', condition: "payload.status !== 'Complete'", itemVariable: 'attempt' }),
|
|
44
|
+
});
|
|
45
|
+
expect(ValidateLoopStep(s, 0)).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('accepts a Configuration supplied as an object, not only as JSON text', () => {
|
|
49
|
+
// A model that was just shown an object literal in the prompt will send one. Rejecting that
|
|
50
|
+
// would make the prompt and the validator disagree about the same example.
|
|
51
|
+
const s = step({ Configuration: { type: 'ForEach', collectionPath: 'leads', itemVariable: 'lead' } as unknown as string });
|
|
52
|
+
expect(ValidateLoopStep(s, 0)).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('ignores non-loop steps entirely', () => {
|
|
56
|
+
expect(ValidateLoopStep(step({ StepType: 'Action', LoopBodyType: undefined, Configuration: undefined }), 0)).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('ValidateLoopStep — a loop that would silently do nothing', () => {
|
|
61
|
+
it('rejects a ForEach with nothing to iterate over', () => {
|
|
62
|
+
const s = step({ Configuration: JSON.stringify({ type: 'ForEach', itemVariable: 'lead' }) });
|
|
63
|
+
const errors = ValidateLoopStep(s, 2);
|
|
64
|
+
expect(errors).toHaveLength(1);
|
|
65
|
+
expect(errors[0]).toContain('collectionPath');
|
|
66
|
+
expect(errors[0]).toContain('index 2');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('rejects a While with nothing to test', () => {
|
|
70
|
+
const s = step({ StepType: 'While', Configuration: JSON.stringify({ type: 'While', itemVariable: 'attempt' }) });
|
|
71
|
+
const errors = ValidateLoopStep(s, 0);
|
|
72
|
+
expect(errors).toHaveLength(1);
|
|
73
|
+
expect(errors[0]).toContain('condition');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects either kind with no name for the current item', () => {
|
|
77
|
+
const s = step({ Configuration: JSON.stringify({ type: 'ForEach', collectionPath: 'leads' }) });
|
|
78
|
+
expect(ValidateLoopStep(s, 0).join(' ')).toContain('itemVariable');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('rejects a loop with no Configuration at all', () => {
|
|
82
|
+
const s = step({ Configuration: undefined });
|
|
83
|
+
const errors = ValidateLoopStep(s, 0);
|
|
84
|
+
expect(errors).toHaveLength(1);
|
|
85
|
+
expect(errors[0]).toContain('Configuration');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('does not cascade bound errors when the Configuration could not be read', () => {
|
|
89
|
+
// One clear "this is not parseable" beats three downstream complaints about fields that
|
|
90
|
+
// could never have been found.
|
|
91
|
+
const s = step({ Configuration: '{not json' });
|
|
92
|
+
const errors = ValidateLoopStep(s, 0);
|
|
93
|
+
expect(errors).toHaveLength(1);
|
|
94
|
+
expect(errors[0]).toContain('invalid Configuration JSON');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects a Configuration that parses to something other than an object', () => {
|
|
98
|
+
expect(ValidateLoopStep(step({ Configuration: '"just a string"' }), 0)[0]).toContain('not a JSON object');
|
|
99
|
+
expect(ValidateLoopStep(step({ Configuration: '[1,2,3]' }), 0)[0]).toContain('not a JSON object');
|
|
100
|
+
expect(
|
|
101
|
+
ValidateLoopStep(step({ Configuration: [1, 2, 3] as unknown as string }), 0)[0],
|
|
102
|
+
).toContain('array rather than an object');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('ValidateLoopStep — the body', () => {
|
|
107
|
+
it('rejects a loop that does not say what it repeats', () => {
|
|
108
|
+
const errors = ValidateLoopStep(step({ LoopBodyType: undefined }), 0);
|
|
109
|
+
expect(errors).toHaveLength(1);
|
|
110
|
+
expect(errors[0]).toContain('LoopBodyType');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('looks for the body id in the field that body type would normally use', () => {
|
|
114
|
+
// No parallel LoopBodyActionID: an action id lives in ActionID whether or not a loop wraps it.
|
|
115
|
+
const errors = ValidateLoopStep(step({ ActionID: undefined }), 0);
|
|
116
|
+
expect(errors).toHaveLength(1);
|
|
117
|
+
expect(errors[0]).toContain('ActionID');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('rejects a Prompt body with neither a prompt to use nor one to create', () => {
|
|
121
|
+
const s = step({ LoopBodyType: 'Prompt', ActionID: undefined });
|
|
122
|
+
expect(ValidateLoopStep(s, 0)[0]).toContain('PromptID');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('accepts a Prompt body supplied inline, which becomes a prompt on save', () => {
|
|
126
|
+
const s = step({ LoopBodyType: 'Prompt', ActionID: undefined, PromptText: 'Summarise this lead.' });
|
|
127
|
+
expect(ValidateLoopStep(s, 0)).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('treats whitespace-only inline prompt text as absent', () => {
|
|
131
|
+
const s = step({ LoopBodyType: 'Prompt', ActionID: undefined, PromptText: ' ' });
|
|
132
|
+
expect(ValidateLoopStep(s, 0)[0]).toContain('PromptID');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('accepts a Sub-Agent body with no id, because it is linked by name after the sub-agent is created', () => {
|
|
136
|
+
// Same latitude a plain Sub-Agent step already gets — requiring an id here would make it
|
|
137
|
+
// impossible to author a loop over a sub-agent being created in the same spec.
|
|
138
|
+
const s = step({ LoopBodyType: 'Sub-Agent', ActionID: undefined, SubAgentID: '' });
|
|
139
|
+
expect(ValidateLoopStep(s, 0)).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('ValidateLoopStep — reporting', () => {
|
|
144
|
+
it('returns every problem at once rather than only the first', () => {
|
|
145
|
+
// One pass gives the model everything it must fix. Reporting one error per round trip turns
|
|
146
|
+
// a single correction into a retry loop.
|
|
147
|
+
const s = step({ LoopBodyType: undefined, ActionID: undefined, Configuration: JSON.stringify({}) });
|
|
148
|
+
const errors = ValidateLoopStep(s, 0);
|
|
149
|
+
expect(errors.length).toBeGreaterThanOrEqual(3);
|
|
150
|
+
expect(errors.join(' ')).toContain('LoopBodyType');
|
|
151
|
+
expect(errors.join(' ')).toContain('collectionPath');
|
|
152
|
+
expect(errors.join(' ')).toContain('itemVariable');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('names the step and its index so the model can find it', () => {
|
|
156
|
+
const errors = ValidateLoopStep(step({ Name: 'Retry the import', Configuration: undefined }), 7);
|
|
157
|
+
expect(errors[0]).toContain('"Retry the import"');
|
|
158
|
+
expect(errors[0]).toContain('index 7');
|
|
159
|
+
expect(errors[0]).toContain('ForEach');
|
|
160
|
+
});
|
|
161
|
+
});
|
package/src/agent-spec-sync.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
AgentSpec,
|
|
17
17
|
AgentActionSpec,
|
|
18
|
+
AgentStep,
|
|
18
19
|
SubAgentSpec
|
|
19
20
|
} from '@memberjunction/ai-core-plus';
|
|
20
21
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
@@ -553,7 +554,7 @@ export class AgentSpecSync {
|
|
|
553
554
|
Name: agent.Name || '',
|
|
554
555
|
Description: agent.Description || undefined,
|
|
555
556
|
TypeID: agent.TypeID || undefined,
|
|
556
|
-
Status:
|
|
557
|
+
Status: agent.Status || undefined,
|
|
557
558
|
IconClass: agent.IconClass || undefined,
|
|
558
559
|
LogoURL: agent.LogoURL || undefined,
|
|
559
560
|
ParentID: agent.ParentID || undefined,
|
|
@@ -718,7 +719,7 @@ export class AgentSpecSync {
|
|
|
718
719
|
* @param step - The agent step entity
|
|
719
720
|
* @returns Mapped step spec
|
|
720
721
|
*/
|
|
721
|
-
private mapStepEntityToSpec(step: MJAIAgentStepEntity):
|
|
722
|
+
private mapStepEntityToSpec(step: MJAIAgentStepEntity): AgentStep {
|
|
722
723
|
return {
|
|
723
724
|
ID: step.ID,
|
|
724
725
|
Name: step.Name,
|
|
@@ -728,8 +729,13 @@ export class AgentSpecSync {
|
|
|
728
729
|
ActionID: step.ActionID || undefined,
|
|
729
730
|
SubAgentID: step.SubAgentID || undefined,
|
|
730
731
|
PromptID: step.PromptID || undefined,
|
|
731
|
-
|
|
732
|
-
|
|
732
|
+
// Loop steps round-trip their body type and bounds; without these, reading a flow agent
|
|
733
|
+
// that contains a loop and writing it back would silently turn the loop into an
|
|
734
|
+
// unconfigured step that iterates over nothing.
|
|
735
|
+
LoopBodyType: step.LoopBodyType || undefined,
|
|
736
|
+
Configuration: step.Configuration || undefined,
|
|
737
|
+
ActionInputMapping: this.parseJsonField<Record<string, unknown>>(step.ActionInputMapping),
|
|
738
|
+
ActionOutputMapping: this.parseJsonField<Record<string, unknown>>(step.ActionOutputMapping)
|
|
733
739
|
};
|
|
734
740
|
}
|
|
735
741
|
|
|
@@ -882,12 +888,9 @@ export class AgentSpecSync {
|
|
|
882
888
|
agentEntity.TypeID = (this.spec as any).TypeID;
|
|
883
889
|
}
|
|
884
890
|
|
|
885
|
-
// Handle Status - defaults to Active if not specified
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
} else {
|
|
889
|
-
agentEntity.Status = 'Active';
|
|
890
|
-
}
|
|
891
|
+
// Handle Status - defaults to Active if not specified. Typed rather than cast: the cast is
|
|
892
|
+
// what let `'Inactive'` — a value the CHECK constraint rejects — reach the entity unchallenged.
|
|
893
|
+
agentEntity.Status = this.spec.Status ?? 'Active';
|
|
891
894
|
|
|
892
895
|
// Serialize JSON fields
|
|
893
896
|
agentEntity.PayloadDownstreamPaths = JSON.stringify(
|
|
@@ -1362,6 +1365,11 @@ export class AgentSpecSync {
|
|
|
1362
1365
|
stepEntity.ActionID = stepSpec.ActionID || null;
|
|
1363
1366
|
stepEntity.SubAgentID = stepSpec.SubAgentID || null;
|
|
1364
1367
|
stepEntity.Status = 'Active'; // Default to Active
|
|
1368
|
+
// Loop steps: the body type says which of Action/Prompt/Sub-Agent runs each pass, and
|
|
1369
|
+
// Configuration carries the bounds. Written for every step because a step that STOPS
|
|
1370
|
+
// being a loop must have these cleared, not left behind from its previous shape.
|
|
1371
|
+
stepEntity.LoopBodyType = stepSpec.LoopBodyType || null;
|
|
1372
|
+
stepEntity.Configuration = stepSpec.Configuration || null;
|
|
1365
1373
|
|
|
1366
1374
|
// Handle inline prompt creation for Prompt-type steps
|
|
1367
1375
|
// If StepType is Prompt and PromptID is empty, create a new AIPrompt record
|
|
@@ -3,6 +3,7 @@ import { ExecuteAgentParams, BaseAgentNextStep, AgentSpec, MJAIAgentRunEntityExt
|
|
|
3
3
|
import { MJActionEntity } from "@memberjunction/core-entities";
|
|
4
4
|
import { RunView } from '@memberjunction/core';
|
|
5
5
|
import { RegisterClass, NormalizeUUID } from '@memberjunction/global';
|
|
6
|
+
import { IsLoopStep, ValidateLoopStep } from '../flow-step-validation';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Architect Agent - Transforms technical design into validated AgentSpec JSON
|
|
@@ -153,8 +154,10 @@ export class AgentArchitectAgent extends BaseAgent {
|
|
|
153
154
|
specWithType.Status = 'Active';
|
|
154
155
|
corrected = true;
|
|
155
156
|
console.log('✓ Auto-corrected: Added default Status = "Active"');
|
|
156
|
-
} else if (!['Active', '
|
|
157
|
-
|
|
157
|
+
} else if (!['Active', 'Disabled', 'Pending'].includes(specWithType.Status)) {
|
|
158
|
+
// 'Disabled', not 'Inactive' — this validator used to accept a value the AIAgent CHECK
|
|
159
|
+
// constraint rejects, so a spec it passed could still fail at save with no explanation.
|
|
160
|
+
errors.push(`❌ Status must be "Active", "Disabled", or "Pending", got: "${specWithType.Status}"`);
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
// 5. Validate agent type-specific requirements
|
|
@@ -247,6 +250,17 @@ export class AgentArchitectAgent extends BaseAgent {
|
|
|
247
250
|
// SubAgentID can be empty "" for new sub-agents (will be linked by name matching)
|
|
248
251
|
// No validation needed here - linking happens in AgentSpecSync
|
|
249
252
|
}
|
|
253
|
+
|
|
254
|
+
// Validate loop steps (ForEach / While)
|
|
255
|
+
//
|
|
256
|
+
// A loop is a wrapper, not a leaf: LoopBodyType names which of Action/Prompt/
|
|
257
|
+
// Sub-Agent runs each pass, and Configuration carries the bounds. A loop missing
|
|
258
|
+
// either is the failure mode worth catching here — it saves cleanly and then
|
|
259
|
+
// iterates over nothing at runtime, which looks like the agent doing no work
|
|
260
|
+
// rather than like a malformed step.
|
|
261
|
+
if (IsLoopStep(step)) {
|
|
262
|
+
errors.push(...ValidateLoopStep(step, i));
|
|
263
|
+
}
|
|
250
264
|
}
|
|
251
265
|
}
|
|
252
266
|
}
|