newo 3.7.3 → 3.7.5
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/CHANGELOG.md +35 -2
- package/README.md +71 -2
- package/dist/cli/commands/get-skill.d.ts +3 -0
- package/dist/cli/commands/get-skill.js +72 -0
- package/dist/cli/commands/help.js +29 -0
- package/dist/cli/commands/logs.d.ts +6 -1
- package/dist/cli/commands/logs.js +62 -14
- package/dist/cli/commands/sandbox.d.ts +10 -4
- package/dist/cli/commands/sandbox.js +182 -51
- package/dist/cli/commands/update-skill.d.ts +4 -0
- package/dist/cli/commands/update-skill.js +119 -0
- package/dist/cli.js +8 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.d.ts +37 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.js +383 -24
- package/dist/sandbox/chat.d.ts +23 -3
- package/dist/sandbox/chat.js +83 -30
- package/dist/sync/remote-skill.d.ts +33 -0
- package/dist/sync/remote-skill.js +52 -0
- package/package.json +1 -1
- package/src/cli/commands/get-skill.ts +84 -0
- package/src/cli/commands/help.ts +29 -0
- package/src/cli/commands/logs.ts +83 -15
- package/src/cli/commands/sandbox.ts +238 -60
- package/src/cli/commands/update-skill.ts +139 -0
- package/src/cli.ts +10 -0
- package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +530 -26
- package/src/sandbox/chat.ts +106 -29
- package/src/sync/remote-skill.ts +92 -0
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* skills/{SkillIdn}.nsl|.nslg
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'fs-extra';
|
|
17
|
-
import
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, createSkill, createSkillParameter, updateSkill, publishFlow, getProjectAttributes, getCustomerAttributes, listLibraries, updateLibrarySkill, getFlow, } from '../../../api.js';
|
|
18
19
|
import { syncFlowMetadata, emptyFlowSyncCounts, totalFlowSyncOps, describeFlowSyncCounts } from '../../../sync/flow-metadata.js';
|
|
19
20
|
import { ensureStateOnly, writeFileSafe, mapPath, } from '../../../fsutil.js';
|
|
20
21
|
import { sha256, saveHashes, loadHashes } from '../../../hash.js';
|
|
21
|
-
import { v2ImportVersionPath, v2ProjectYamlPath, v2AgentYamlPath, v2FlowYamlPath, v2SkillScriptPath, v2SkillRelativePath, v2ProjectAttributesPath, v2CustomerAttributesPath, v2AkbDir, v2AkbPath, v2LibraryYamlPath, v2LibrarySkillScriptPath, v2LibrarySkillRelativePath, } from '../../../format/paths-v2.js';
|
|
22
|
+
import { v2ImportVersionPath, v2ProjectYamlPath, v2AgentDir, v2AgentYamlPath, v2FlowYamlPath, v2SkillScriptPath, v2SkillRelativePath, v2ProjectAttributesPath, v2CustomerAttributesPath, v2AkbDir, v2AkbPath, v2LibraryYamlPath, v2LibrarySkillScriptPath, v2LibrarySkillRelativePath, } from '../../../format/paths-v2.js';
|
|
22
23
|
import { V2_IMPORT_VERSION, } from '../../../format/types.js';
|
|
23
|
-
import { generateV2FlowYaml, generateV2ProjectYaml, generateV2AgentYaml, buildV2InlineSkill, buildV2FlowEvent, buildV2StateField,
|
|
24
|
+
import { generateV2FlowYaml, generateV2ProjectYaml, generateV2AgentYaml, parseV2FlowYaml, buildV2InlineSkill, buildV2FlowEvent, buildV2StateField, } from '../../../format/v2-yaml.js';
|
|
24
25
|
import { isContentDifferent } from '../../../sync/skill-files.js';
|
|
25
26
|
import yaml from 'js-yaml';
|
|
26
27
|
import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
|
|
@@ -438,8 +439,15 @@ export class V2ProjectSyncStrategy {
|
|
|
438
439
|
return result;
|
|
439
440
|
}
|
|
440
441
|
const mapData = await fs.readJson(mapFile);
|
|
442
|
+
const metadataSync = await this.syncV2FlowYamlDefinitions(client, customer, mapData, newHashes);
|
|
443
|
+
result.created += metadataSync.created;
|
|
444
|
+
result.updated += metadataSync.updated;
|
|
445
|
+
result.errors.push(...metadataSync.errors);
|
|
441
446
|
for (const change of changes) {
|
|
442
447
|
try {
|
|
448
|
+
if (metadataSync.syncedPaths.has(change.path)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
443
451
|
if (change.operation === 'modified') {
|
|
444
452
|
// V2 flow YAML: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/{flow}.yaml
|
|
445
453
|
// The flow YAML carries title, events, and state_fields inline, so
|
|
@@ -453,7 +461,7 @@ export class V2ProjectSyncStrategy {
|
|
|
453
461
|
const isLibrary = change.path.includes('/libraries/');
|
|
454
462
|
const count = isLibrary
|
|
455
463
|
? await this.pushV2LibrarySkillUpdate(client, change, mapData, newHashes)
|
|
456
|
-
: await this.pushV2SkillUpdate(client, change, mapData, newHashes);
|
|
464
|
+
: await this.pushV2SkillUpdate(client, change, mapData, newHashes, customer.idn);
|
|
457
465
|
result.updated += count;
|
|
458
466
|
}
|
|
459
467
|
}
|
|
@@ -461,6 +469,9 @@ export class V2ProjectSyncStrategy {
|
|
|
461
469
|
result.errors.push(`Failed to push ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
462
470
|
}
|
|
463
471
|
}
|
|
472
|
+
if (metadataSync.created > 0 || metadataSync.updated > 0) {
|
|
473
|
+
await writeFileSafe(mapFile, JSON.stringify(mapData, null, 2));
|
|
474
|
+
}
|
|
464
475
|
await saveHashes(newHashes, customer.idn);
|
|
465
476
|
if (result.created > 0 || result.updated > 0) {
|
|
466
477
|
await this.publishAllFlows(client, mapData);
|
|
@@ -564,28 +575,256 @@ export class V2ProjectSyncStrategy {
|
|
|
564
575
|
newHashes[change.path] = sha256(content);
|
|
565
576
|
return total;
|
|
566
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Reconcile inline skill definitions from V2 flow YAML before pushing scripts.
|
|
580
|
+
*
|
|
581
|
+
* V2 keeps skill metadata (model, runner_type, parameters) in the flow YAML,
|
|
582
|
+
* not in a separate skill metadata file. The map only contains the remote IDs
|
|
583
|
+
* from a previous pull, so new local skills must be created before their
|
|
584
|
+
* callers can be published.
|
|
585
|
+
*/
|
|
586
|
+
async syncV2FlowYamlDefinitions(client, customer, mapData, newHashes) {
|
|
587
|
+
let created = 0;
|
|
588
|
+
let updated = 0;
|
|
589
|
+
const syncedPaths = new Set();
|
|
590
|
+
const errors = [];
|
|
591
|
+
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
592
|
+
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
593
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
594
|
+
const flowYamlPath = v2FlowYamlPath(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
595
|
+
if (!(await fs.pathExists(flowYamlPath))) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
let flowDef;
|
|
599
|
+
try {
|
|
600
|
+
flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
this.logger.warn(`[newo_v2] Failed to parse flow YAML ${flowYamlPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
for (const skill of flowDef.skills || []) {
|
|
607
|
+
const skillLocator = `${projectIdn}/${agentIdn}/${flowIdn}/${skill.idn}`;
|
|
608
|
+
// Per-skill failure isolation: one broken skill must not abort the
|
|
609
|
+
// push of every other project/flow in the workspace.
|
|
610
|
+
try {
|
|
611
|
+
const runnerType = this.normalizeRunnerType(skill.runner_type);
|
|
612
|
+
const scriptPath = await this.resolveV2FlowSkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skill.idn, runnerType, skill.prompt_script);
|
|
613
|
+
if (!(await fs.pathExists(scriptPath))) {
|
|
614
|
+
errors.push(`[newo_v2] Missing script for skill ${skillLocator}: ${scriptPath}`);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const content = await fs.readFile(scriptPath, 'utf8');
|
|
618
|
+
const localMetadata = this.buildV2SkillMetadataFromYaml(skill, flowDef, runnerType, flowData.skills[skill.idn]);
|
|
619
|
+
const existingSkill = flowData.skills[skill.idn];
|
|
620
|
+
if (!existingSkill) {
|
|
621
|
+
this.assertSkillModelResolved(localMetadata, skillLocator);
|
|
622
|
+
try {
|
|
623
|
+
const createdSkill = await createSkill(client, flowData.id, {
|
|
624
|
+
idn: localMetadata.idn,
|
|
625
|
+
title: localMetadata.title,
|
|
626
|
+
prompt_script: content,
|
|
627
|
+
runner_type: localMetadata.runner_type,
|
|
628
|
+
model: localMetadata.model,
|
|
629
|
+
parameters: localMetadata.parameters,
|
|
630
|
+
path: localMetadata.path || ''
|
|
631
|
+
});
|
|
632
|
+
// The create endpoint ignores inline `parameters` (verified
|
|
633
|
+
// against the live platform) — create them explicitly.
|
|
634
|
+
await this.createMissingSkillParameters(client, { ...localMetadata, id: createdSkill.id, parameters: [] }, localMetadata);
|
|
635
|
+
flowData.skills[skill.idn] = {
|
|
636
|
+
...localMetadata,
|
|
637
|
+
id: createdSkill.id
|
|
638
|
+
};
|
|
639
|
+
newHashes[scriptPath] = sha256(content);
|
|
640
|
+
syncedPaths.add(scriptPath);
|
|
641
|
+
created++;
|
|
642
|
+
this.logger.info(`[newo_v2] Created skill: ${flowIdn}/${skill.idn}`);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
if (!this.isAlreadyExistsApiError(error)) {
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
const remoteSkills = await listFlowSkills(client, flowData.id);
|
|
649
|
+
const remoteSkill = remoteSkills.find(s => s.idn === skill.idn);
|
|
650
|
+
if (!remoteSkill) {
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
const remoteMetadata = {
|
|
654
|
+
id: remoteSkill.id,
|
|
655
|
+
idn: remoteSkill.idn,
|
|
656
|
+
title: remoteSkill.title,
|
|
657
|
+
runner_type: remoteSkill.runner_type,
|
|
658
|
+
model: remoteSkill.model,
|
|
659
|
+
parameters: this.normalizeParameters(remoteSkill.parameters),
|
|
660
|
+
path: remoteSkill.path
|
|
661
|
+
};
|
|
662
|
+
await this.createMissingSkillParameters(client, remoteMetadata, localMetadata);
|
|
663
|
+
await updateSkill(client, {
|
|
664
|
+
id: remoteSkill.id,
|
|
665
|
+
title: localMetadata.title,
|
|
666
|
+
idn: localMetadata.idn,
|
|
667
|
+
prompt_script: content,
|
|
668
|
+
runner_type: localMetadata.runner_type,
|
|
669
|
+
model: localMetadata.model,
|
|
670
|
+
parameters: localMetadata.parameters,
|
|
671
|
+
path: remoteSkill.path || localMetadata.path
|
|
672
|
+
});
|
|
673
|
+
flowData.skills[skill.idn] = {
|
|
674
|
+
...localMetadata,
|
|
675
|
+
id: remoteSkill.id,
|
|
676
|
+
path: remoteSkill.path || localMetadata.path
|
|
677
|
+
};
|
|
678
|
+
newHashes[scriptPath] = sha256(content);
|
|
679
|
+
syncedPaths.add(scriptPath);
|
|
680
|
+
updated++;
|
|
681
|
+
this.logger.info(`[newo_v2] Reused existing skill: ${flowIdn}/${skill.idn}`);
|
|
682
|
+
}
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const createdParameters = await this.createMissingSkillParameters(client, existingSkill, localMetadata);
|
|
686
|
+
if (createdParameters > 0 || this.skillMetadataDiffers(existingSkill, localMetadata)) {
|
|
687
|
+
this.assertSkillModelResolved(localMetadata, skillLocator);
|
|
688
|
+
await updateSkill(client, {
|
|
689
|
+
id: existingSkill.id,
|
|
690
|
+
title: localMetadata.title,
|
|
691
|
+
idn: localMetadata.idn,
|
|
692
|
+
prompt_script: content,
|
|
693
|
+
runner_type: localMetadata.runner_type,
|
|
694
|
+
model: localMetadata.model,
|
|
695
|
+
parameters: localMetadata.parameters,
|
|
696
|
+
path: localMetadata.path
|
|
697
|
+
});
|
|
698
|
+
flowData.skills[skill.idn] = {
|
|
699
|
+
...localMetadata,
|
|
700
|
+
id: existingSkill.id
|
|
701
|
+
};
|
|
702
|
+
newHashes[scriptPath] = sha256(content);
|
|
703
|
+
syncedPaths.add(scriptPath);
|
|
704
|
+
updated++;
|
|
705
|
+
this.logger.info(`[newo_v2] Updated skill metadata: ${flowIdn}/${skill.idn}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
errors.push(`Failed to sync skill ${skillLocator}: ${error instanceof Error ? error.message : String(error)}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return { created, updated, syncedPaths, errors };
|
|
716
|
+
}
|
|
717
|
+
async createMissingSkillParameters(client, existing, local) {
|
|
718
|
+
const existingNames = new Set(this.normalizeParameters(existing.parameters).map(p => p.name));
|
|
719
|
+
let created = 0;
|
|
720
|
+
for (const parameter of local.parameters) {
|
|
721
|
+
if (existingNames.has(parameter.name)) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
await createSkillParameter(client, existing.id, {
|
|
726
|
+
name: parameter.name,
|
|
727
|
+
default_value: parameter.default_value ?? ''
|
|
728
|
+
});
|
|
729
|
+
created++;
|
|
730
|
+
this.logger.info(`[newo_v2] Created skill parameter: ${local.idn}/${parameter.name}`);
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
if (!this.isAlreadyExistsApiError(error)) {
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
existingNames.add(parameter.name);
|
|
738
|
+
}
|
|
739
|
+
return created;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Detect "resource already exists" API errors.
|
|
743
|
+
*
|
|
744
|
+
* Matches only on the precise phrases the platform actually returns
|
|
745
|
+
* ("already exists", "duplicate key"). Loose substrings like "exist"
|
|
746
|
+
* would otherwise sweep up unrelated "does not exist" / "doesn't exist"
|
|
747
|
+
* errors and trigger an incorrect reuse fallback.
|
|
748
|
+
*/
|
|
749
|
+
isAlreadyExistsApiError(error) {
|
|
750
|
+
const response = error?.response;
|
|
751
|
+
const status = response?.status;
|
|
752
|
+
if (status !== 400 && status !== 409 && status !== 422) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
const haystack = JSON.stringify(response?.data ?? (error instanceof Error ? error.message : String(error))).toLowerCase();
|
|
756
|
+
return haystack.includes('already exists') || haystack.includes('duplicate key');
|
|
757
|
+
}
|
|
758
|
+
normalizeRunnerType(runnerType) {
|
|
759
|
+
return runnerType === 'nsl' ? 'nsl' : 'guidance';
|
|
760
|
+
}
|
|
761
|
+
normalizeParameters(parameters) {
|
|
762
|
+
return (parameters || []).map(p => ({
|
|
763
|
+
name: p.name,
|
|
764
|
+
default_value: p.default_value ?? ''
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Fail fast if no model could be resolved for a V2 skill.
|
|
769
|
+
*
|
|
770
|
+
* `buildV2SkillMetadataFromYaml` falls back to empty strings when neither
|
|
771
|
+
* the skill nor the flow declare a model. The platform rejects empty
|
|
772
|
+
* model_idn/provider_idn at creation/update time, but the error it returns
|
|
773
|
+
* is generic — we surface a clearer message before issuing the request.
|
|
774
|
+
*/
|
|
775
|
+
assertSkillModelResolved(metadata, locator) {
|
|
776
|
+
if (!metadata.model.model_idn || !metadata.model.provider_idn) {
|
|
777
|
+
throw new Error(`[newo_v2] Cannot resolve model for skill ${locator}: ` +
|
|
778
|
+
`model_idn="${metadata.model.model_idn}", provider_idn="${metadata.model.provider_idn}". ` +
|
|
779
|
+
`Set either skill.model.* or flow default_model_idn/default_provider_idn in the flow YAML.`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
buildV2SkillMetadataFromYaml(skill, flowDef, runnerType, existing) {
|
|
783
|
+
return {
|
|
784
|
+
id: existing?.id || '',
|
|
785
|
+
idn: skill.idn,
|
|
786
|
+
title: skill.title || '',
|
|
787
|
+
runner_type: runnerType,
|
|
788
|
+
model: {
|
|
789
|
+
model_idn: skill.model?.model_idn || flowDef.default_model_idn || '',
|
|
790
|
+
provider_idn: skill.model?.provider_idn || flowDef.default_provider_idn || ''
|
|
791
|
+
},
|
|
792
|
+
parameters: this.normalizeParameters(skill.parameters),
|
|
793
|
+
path: existing?.path || ''
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
skillMetadataDiffers(existing, local) {
|
|
797
|
+
// Compare model/parameters field-by-field, never via JSON.stringify of the
|
|
798
|
+
// raw objects: the map stores model keys in platform API order
|
|
799
|
+
// (provider_idn first) while YAML-built metadata uses model_idn first, and
|
|
800
|
+
// a key-order-sensitive comparison flags every skill as changed.
|
|
801
|
+
const paramsKey = (params) => JSON.stringify(this.normalizeParameters(params).sort((a, b) => a.name.localeCompare(b.name)));
|
|
802
|
+
return (existing.title !== local.title ||
|
|
803
|
+
existing.runner_type !== local.runner_type ||
|
|
804
|
+
existing.model.model_idn !== local.model.model_idn ||
|
|
805
|
+
existing.model.provider_idn !== local.model.provider_idn ||
|
|
806
|
+
paramsKey(existing.parameters) !== paramsKey(local.parameters));
|
|
807
|
+
}
|
|
808
|
+
async resolveV2FlowSkillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType, promptScript) {
|
|
809
|
+
if (promptScript) {
|
|
810
|
+
const fromPromptScript = `${v2AgentDir(customerIdn, projectIdn, agentIdn)}/${promptScript}`;
|
|
811
|
+
if (await fs.pathExists(fromPromptScript)) {
|
|
812
|
+
return fromPromptScript;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return v2SkillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType);
|
|
816
|
+
}
|
|
567
817
|
/**
|
|
568
818
|
* Push a V2 skill update
|
|
569
819
|
*
|
|
570
820
|
* V2 path: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skill}.nsl
|
|
571
821
|
*/
|
|
572
|
-
async pushV2SkillUpdate(client, change, mapData, newHashes) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
579
|
-
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
580
|
-
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
581
|
-
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
582
|
-
// Look up skill in map
|
|
583
|
-
const projectData = mapData.projects[projectIdn];
|
|
584
|
-
const agentData = projectData?.agents[agentIdn];
|
|
585
|
-
const flowData = agentData?.flows[flowIdn];
|
|
586
|
-
const skillData = flowData?.skills[skillIdn];
|
|
587
|
-
if (!skillData) {
|
|
588
|
-
throw new Error(`Skill ${skillIdn} not found in project map (path: ${change.path})`);
|
|
822
|
+
async pushV2SkillUpdate(client, change, mapData, newHashes, customerIdn) {
|
|
823
|
+
const target = await this.resolveV2SkillTargetForScriptPath(customerIdn, change.path, mapData) ||
|
|
824
|
+
this.resolveV2SkillTargetFromCanonicalPath(change.path, mapData);
|
|
825
|
+
const skillData = target?.skillData;
|
|
826
|
+
if (!target || !skillData) {
|
|
827
|
+
throw new Error(`Skill not found in project map (path: ${change.path})`);
|
|
589
828
|
}
|
|
590
829
|
// Read updated script content
|
|
591
830
|
const content = await fs.readFile(change.path, 'utf8');
|
|
@@ -601,9 +840,67 @@ export class V2ProjectSyncStrategy {
|
|
|
601
840
|
path: skillData.path
|
|
602
841
|
});
|
|
603
842
|
newHashes[change.path] = sha256(content);
|
|
604
|
-
this.logger.info(`[newo_v2] Pushed: ${skillIdn}`);
|
|
843
|
+
this.logger.info(`[newo_v2] Pushed: ${target.skillIdn}`);
|
|
605
844
|
return 1;
|
|
606
845
|
}
|
|
846
|
+
normalizePathForComparison(filePath) {
|
|
847
|
+
return path.resolve(filePath).replace(/\\/g, '/');
|
|
848
|
+
}
|
|
849
|
+
async resolveV2SkillTargetForScriptPath(customerIdn, scriptPath, mapData) {
|
|
850
|
+
const normalizedScriptPath = this.normalizePathForComparison(scriptPath);
|
|
851
|
+
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
852
|
+
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
853
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
854
|
+
const flowYamlPath = v2FlowYamlPath(customerIdn, projectIdn, agentIdn, flowIdn);
|
|
855
|
+
if (!(await fs.pathExists(flowYamlPath))) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
let flowDef;
|
|
859
|
+
try {
|
|
860
|
+
flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
for (const skill of flowDef.skills || []) {
|
|
866
|
+
const runnerType = this.normalizeRunnerType(skill.runner_type || flowData.skills[skill.idn]?.runner_type);
|
|
867
|
+
const resolvedScriptPath = await this.resolveV2FlowSkillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skill.idn, runnerType, skill.prompt_script);
|
|
868
|
+
if (this.normalizePathForComparison(resolvedScriptPath) === normalizedScriptPath) {
|
|
869
|
+
return {
|
|
870
|
+
projectIdn,
|
|
871
|
+
agentIdn,
|
|
872
|
+
flowIdn,
|
|
873
|
+
skillIdn: skill.idn,
|
|
874
|
+
skillData: flowData.skills[skill.idn]
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
resolveV2SkillTargetFromCanonicalPath(scriptPath, mapData) {
|
|
884
|
+
// Parse canonical V2 path:
|
|
885
|
+
// .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
|
|
886
|
+
const pathParts = scriptPath.split('/');
|
|
887
|
+
const skillFileName = pathParts[pathParts.length - 1] || '';
|
|
888
|
+
const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
|
|
889
|
+
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
890
|
+
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
891
|
+
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
892
|
+
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
893
|
+
const projectData = mapData.projects[projectIdn];
|
|
894
|
+
const agentData = projectData?.agents[agentIdn];
|
|
895
|
+
const flowData = agentData?.flows[flowIdn];
|
|
896
|
+
return {
|
|
897
|
+
projectIdn,
|
|
898
|
+
agentIdn,
|
|
899
|
+
flowIdn,
|
|
900
|
+
skillIdn,
|
|
901
|
+
skillData: flowData?.skills[skillIdn]
|
|
902
|
+
};
|
|
903
|
+
}
|
|
607
904
|
/**
|
|
608
905
|
* Push a V2 library skill update
|
|
609
906
|
* Path: .../newo_customers/{cust}/{proj}/libraries/{lib}/skills/{skillFile}
|
|
@@ -686,8 +983,18 @@ export class V2ProjectSyncStrategy {
|
|
|
686
983
|
}
|
|
687
984
|
}
|
|
688
985
|
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
689
|
-
|
|
690
|
-
|
|
986
|
+
const flowYamlSkills = await this.loadLocalV2FlowSkills(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
987
|
+
const skillIdns = new Set([
|
|
988
|
+
...Object.keys(flowData.skills),
|
|
989
|
+
...flowYamlSkills.keys()
|
|
990
|
+
]);
|
|
991
|
+
for (const skillIdn of skillIdns) {
|
|
992
|
+
const yamlSkill = flowYamlSkills.get(skillIdn);
|
|
993
|
+
const skillMeta = flowData.skills[skillIdn];
|
|
994
|
+
const runnerType = this.normalizeRunnerType(yamlSkill?.runner_type || skillMeta?.runner_type);
|
|
995
|
+
const scriptPath = yamlSkill
|
|
996
|
+
? await this.resolveV2FlowSkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType, yamlSkill.prompt_script)
|
|
997
|
+
: v2SkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType);
|
|
691
998
|
if (await fs.pathExists(scriptPath)) {
|
|
692
999
|
const content = await fs.readFile(scriptPath, 'utf8');
|
|
693
1000
|
const currentHash = sha256(content);
|
|
@@ -726,6 +1033,19 @@ export class V2ProjectSyncStrategy {
|
|
|
726
1033
|
}
|
|
727
1034
|
return changes;
|
|
728
1035
|
}
|
|
1036
|
+
async loadLocalV2FlowSkills(customerIdn, projectIdn, agentIdn, flowIdn) {
|
|
1037
|
+
const flowYamlPath = v2FlowYamlPath(customerIdn, projectIdn, agentIdn, flowIdn);
|
|
1038
|
+
if (!(await fs.pathExists(flowYamlPath))) {
|
|
1039
|
+
return new Map();
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
const flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
1043
|
+
return new Map((flowDef.skills || []).map(skill => [skill.idn, skill]));
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
return new Map();
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
729
1049
|
async validate(customer, _items) {
|
|
730
1050
|
const errors = [];
|
|
731
1051
|
const mapFile = mapPath(customer.idn);
|
|
@@ -741,7 +1061,46 @@ export class V2ProjectSyncStrategy {
|
|
|
741
1061
|
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
742
1062
|
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
743
1063
|
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
1064
|
+
const flowYamlPath = v2FlowYamlPath(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
1065
|
+
let localYamlSkills;
|
|
1066
|
+
if (await fs.pathExists(flowYamlPath)) {
|
|
1067
|
+
try {
|
|
1068
|
+
const flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
1069
|
+
localYamlSkills = new Map((flowDef.skills || []).map(s => [s.idn, s]));
|
|
1070
|
+
const skillIdns = new Set([
|
|
1071
|
+
...Object.keys(flowData.skills),
|
|
1072
|
+
...localYamlSkills.keys()
|
|
1073
|
+
]);
|
|
1074
|
+
for (const skillIdn of skillIdns) {
|
|
1075
|
+
const localYamlSkill = localYamlSkills.get(skillIdn);
|
|
1076
|
+
const skillMeta = flowData.skills[skillIdn];
|
|
1077
|
+
if (!localYamlSkill) {
|
|
1078
|
+
errors.push({
|
|
1079
|
+
field: `skill.${skillIdn}`,
|
|
1080
|
+
message: `Skill exists in project map but is missing from flow YAML: ${flowYamlPath}`,
|
|
1081
|
+
path: flowYamlPath
|
|
1082
|
+
});
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const runnerType = this.normalizeRunnerType(localYamlSkill.runner_type || skillMeta?.runner_type);
|
|
1086
|
+
const scriptPath = await this.resolveV2FlowSkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType, localYamlSkill.prompt_script);
|
|
1087
|
+
if (!(await fs.pathExists(scriptPath))) {
|
|
1088
|
+
errors.push({
|
|
1089
|
+
field: `skill.${localYamlSkill.idn}`,
|
|
1090
|
+
message: `Script file not found: ${scriptPath}`,
|
|
1091
|
+
path: scriptPath
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
localYamlSkills = undefined;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
744
1100
|
for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
|
|
1101
|
+
if (localYamlSkills) {
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
745
1104
|
const scriptPath = v2SkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
746
1105
|
if (!(await fs.pathExists(scriptPath))) {
|
|
747
1106
|
errors.push({
|
package/dist/sandbox/chat.d.ts
CHANGED
|
@@ -5,9 +5,28 @@
|
|
|
5
5
|
import type { AxiosInstance } from 'axios';
|
|
6
6
|
import type { SandboxChatSession, Connector, ConversationAct, ChatDebugInfo } from '../types.js';
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Options for selecting which connector to chat through
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
10
|
+
export interface ConnectorSelectionOptions {
|
|
11
|
+
/** Integration IDN to search connectors in (default: 'sandbox') */
|
|
12
|
+
integrationIdn?: string;
|
|
13
|
+
/** Exact connector_idn to use; when omitted, the first running connector is used */
|
|
14
|
+
connectorIdn?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* List running connectors of an integration (default: sandbox).
|
|
18
|
+
* Used by `newo sandbox --list-connectors` and connector selection.
|
|
19
|
+
*/
|
|
20
|
+
export declare function listRunningSandboxConnectors(client: AxiosInstance, integrationIdn?: string): Promise<Connector[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Find a sandbox connector from the customer's connectors list.
|
|
23
|
+
*
|
|
24
|
+
* Without options, preserves legacy behavior: first running connector of the
|
|
25
|
+
* 'sandbox' integration. With options.connectorIdn, selects that exact
|
|
26
|
+
* connector and throws a descriptive error (listing available connectors)
|
|
27
|
+
* when it is not found or not running.
|
|
28
|
+
*/
|
|
29
|
+
export declare function findSandboxConnector(client: AxiosInstance, verbose?: boolean, options?: ConnectorSelectionOptions): Promise<Connector | null>;
|
|
11
30
|
/**
|
|
12
31
|
* Create a new sandbox chat session
|
|
13
32
|
*/
|
|
@@ -21,9 +40,10 @@ export declare function sendMessage(client: AxiosInstance, session: SandboxChatS
|
|
|
21
40
|
* Poll for new conversation acts (messages and debug info)
|
|
22
41
|
* Continues polling until we get an agent response, not just any new message
|
|
23
42
|
*/
|
|
24
|
-
export declare function pollForResponse(client: AxiosInstance, session: SandboxChatSession, messageSentAt?: Date | null, verbose?: boolean): Promise<{
|
|
43
|
+
export declare function pollForResponse(client: AxiosInstance, session: SandboxChatSession, messageSentAt?: Date | null, verbose?: boolean, timeoutMs?: number): Promise<{
|
|
25
44
|
acts: ConversationAct[];
|
|
26
45
|
agentPersonaId: string | null;
|
|
46
|
+
userAct: ConversationAct | null;
|
|
27
47
|
}>;
|
|
28
48
|
/**
|
|
29
49
|
* Extract agent messages from acts
|